lesli_view 1.1.2 → 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/readme.md +158 -29
- metadata +6 -4
- 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
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/* Tailwind styles for LesliView::Forms builders. */
|
|
2
|
+
|
|
3
|
+
@layer components {
|
|
4
|
+
.lesli-form-field {
|
|
5
|
+
@apply mb-4;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.lesli-form-label-wrapper {
|
|
9
|
+
@apply mb-2;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.lesli-form-label {
|
|
13
|
+
@apply block text-sm font-medium leading-5 text-slate-700;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.lesli-form-field-body {
|
|
17
|
+
@apply min-w-0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.lesli-form-control {
|
|
21
|
+
@apply relative;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.lesli-form-input,
|
|
25
|
+
.lesli-form-select,
|
|
26
|
+
.lesli-form-control textarea,
|
|
27
|
+
.lesli-form-control trix-editor {
|
|
28
|
+
@apply block w-full rounded-xl border border-slate-300 bg-white px-3.5 py-2.5 text-sm text-slate-900 shadow-sm outline-none transition placeholder:text-slate-400 hover:border-slate-400 focus:border-sky-500 focus:ring-4 focus:ring-sky-500/10 disabled:cursor-not-allowed disabled:bg-slate-100 disabled:text-slate-500 disabled:opacity-75;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.lesli-form-input,
|
|
32
|
+
.lesli-form-select {
|
|
33
|
+
@apply h-11;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.lesli-form-control textarea,
|
|
37
|
+
.lesli-form-control trix-editor {
|
|
38
|
+
@apply min-h-32 resize-y;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.lesli-form-select-wrapper {
|
|
42
|
+
@apply relative;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.lesli-form-select {
|
|
46
|
+
@apply cursor-pointer appearance-none pr-10;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.lesli-form-select-wrapper::after {
|
|
50
|
+
@apply pointer-events-none absolute right-3.5 top-1/2 h-2 w-2 -translate-y-2 rotate-45 border-b-2 border-r-2 border-slate-400 content-[''];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.lesli-form-checkbox {
|
|
54
|
+
@apply h-4 w-4 cursor-pointer rounded border-slate-300 accent-sky-600 focus:ring-2 focus:ring-sky-500/30 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.lesli-form-checkbox-label {
|
|
58
|
+
@apply inline-flex cursor-pointer items-center gap-2.5 text-sm text-slate-600;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.lesli-form-control.has-icon .lesli-form-input,
|
|
62
|
+
.lesli-form-control.has-icon .lesli-form-select {
|
|
63
|
+
@apply pl-11;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.lesli-form-icon {
|
|
67
|
+
@apply pointer-events-none absolute left-3.5 top-1/2 inline-flex h-5 w-5 -translate-y-1/2 items-center justify-center text-slate-400;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.lesli-form-icon .material-symbols-outlined {
|
|
71
|
+
@apply text-xl leading-none;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.lesli-form-help {
|
|
75
|
+
@apply mt-2 text-sm leading-5 text-slate-500;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.lesli-form-field.is-danger .lesli-form-input,
|
|
79
|
+
.lesli-form-field.is-danger .lesli-form-select,
|
|
80
|
+
.lesli-form-field.is-danger textarea,
|
|
81
|
+
.lesli-form-field.is-danger trix-editor {
|
|
82
|
+
@apply border-rose-400 focus:border-rose-500 focus:ring-rose-500/10;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.lesli-form-help.is-danger {
|
|
86
|
+
@apply text-rose-700;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.lesli-form-help.is-warning {
|
|
90
|
+
@apply text-amber-700;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.lesli-form-help.is-success {
|
|
94
|
+
@apply text-emerald-700;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.lesli-form-help.is-info {
|
|
98
|
+
@apply text-sky-700;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.lesli-form-button {
|
|
102
|
+
@apply inline-flex h-11 cursor-pointer items-center justify-center gap-2 rounded-xl border border-transparent px-4 text-sm font-semibold shadow-sm transition focus:outline-none focus:ring-4 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.lesli-form-button.is-primary,
|
|
106
|
+
.lesli-form-button.is-info {
|
|
107
|
+
@apply bg-sky-600 text-white hover:bg-sky-700 focus:ring-sky-500/20;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.lesli-form-button.is-success {
|
|
111
|
+
@apply bg-emerald-600 text-white hover:bg-emerald-700 focus:ring-emerald-500/20;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.lesli-form-button.is-warning {
|
|
115
|
+
@apply bg-amber-500 text-amber-950 hover:bg-amber-600 focus:ring-amber-500/20;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.lesli-form-button.is-danger {
|
|
119
|
+
@apply bg-rose-600 text-white hover:bg-rose-700 focus:ring-rose-500/20;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.lesli-form-button.is-fullwidth {
|
|
123
|
+
@apply w-full;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.lesli-form-button-icon {
|
|
127
|
+
@apply inline-flex h-5 w-5 items-center justify-center;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.lesli-form-button-icon .material-symbols {
|
|
131
|
+
@apply text-xl leading-none;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.lesli-form-fieldset {
|
|
135
|
+
@apply mb-6 rounded-2xl border border-slate-200 bg-white p-6 shadow-sm;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.lesli-form-legend {
|
|
139
|
+
@apply -ml-1 mb-5 px-1 text-lg font-semibold text-slate-800;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.lesli-form-fieldset.is-info {
|
|
143
|
+
@apply border-sky-200 bg-sky-50/40;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.lesli-form-fieldset.is-success {
|
|
147
|
+
@apply border-emerald-200 bg-emerald-50/40;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.lesli-form-fieldset.is-warning {
|
|
151
|
+
@apply border-amber-200 bg-amber-50/50;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.lesli-form-fieldset.is-danger {
|
|
155
|
+
@apply border-rose-200 bg-rose-50/40;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
@media (min-width: 768px) {
|
|
159
|
+
.lesli-form-field.is-horizontal {
|
|
160
|
+
@apply grid grid-cols-[minmax(8rem,1fr)_minmax(0,3fr)] items-start gap-5;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.lesli-form-field.is-horizontal > .lesli-form-label-wrapper {
|
|
164
|
+
@apply mb-0 pt-2.5 text-right;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.lesli-form-field.is-horizontal > .lesli-form-field-body {
|
|
168
|
+
@apply col-start-2;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -31,57 +31,5 @@ Building a better future, one line of code at a time.
|
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
33
|
|
|
34
|
-
//
|
|
35
|
-
.
|
|
36
|
-
.field:not(.is-horizontal) {
|
|
37
|
-
.field-label {
|
|
38
|
-
text-align: left;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
.lesli-form-fieldset {
|
|
45
|
-
padding: 2.5rem 2rem;
|
|
46
|
-
border-radius: 5px;
|
|
47
|
-
//border: 1px solid #ccc;
|
|
48
|
-
background-color: #fff;
|
|
49
|
-
box-shadow: rgba(0, 0, 0, 0.16) 0px 1px 4px;
|
|
50
|
-
|
|
51
|
-
fieldset {
|
|
52
|
-
padding: 1.2rem;
|
|
53
|
-
border-radius: 5px;
|
|
54
|
-
border: 1px solid #ccc;
|
|
55
|
-
margin-bottom: 1rem;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
form fieldset.is-warning {
|
|
61
|
-
border: 2px solid var(--bulma-warning);
|
|
62
|
-
background-color: var(--bulma-warning-95);
|
|
63
|
-
}
|
|
64
|
-
form fieldset.is-warning h5 {
|
|
65
|
-
color: var(--bulma-warning-bold);
|
|
66
|
-
font-weight: 600;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
form fieldset.is-info {
|
|
71
|
-
border: 2px solid var(--bulma-info);
|
|
72
|
-
background-color: var(--bulma-info-95);
|
|
73
|
-
}
|
|
74
|
-
form fieldset.is-info h5 {
|
|
75
|
-
color: var(--bulma-info-bold);
|
|
76
|
-
font-weight: 600;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
form fieldset.is-danger {
|
|
81
|
-
border: 2px solid var(--bulma-danger);
|
|
82
|
-
background-color: var(--bulma-danger-95);
|
|
83
|
-
}
|
|
84
|
-
form fieldset.is-danger h5 {
|
|
85
|
-
color: var(--bulma-danger-bold);
|
|
86
|
-
font-weight: 600;
|
|
87
|
-
}
|
|
34
|
+
// LesliView forms have migrated to Tailwind.
|
|
35
|
+
// See lib/lesli_view/forms/form.css.
|
|
@@ -2,17 +2,21 @@ module LesliView
|
|
|
2
2
|
module Forms
|
|
3
3
|
module Inputs
|
|
4
4
|
def label(method, text = nil, options = {})
|
|
5
|
-
|
|
5
|
+
options = options.symbolize_keys
|
|
6
|
+
options[:class] = merge_css_classes("lesli-form-label", options[:class])
|
|
7
|
+
super(method, text, options)
|
|
6
8
|
end
|
|
7
9
|
|
|
8
10
|
def check_box(method, options = {}, checked_value = "1", unchecked_value = "0")
|
|
9
|
-
|
|
11
|
+
options = options.symbolize_keys
|
|
12
|
+
options[:class] = merge_css_classes("lesli-form-checkbox", options[:class])
|
|
10
13
|
super(method, options, checked_value, unchecked_value)
|
|
11
14
|
end
|
|
12
15
|
|
|
13
16
|
def text_field(method, options = {})
|
|
14
|
-
|
|
15
|
-
|
|
17
|
+
options = options.symbolize_keys
|
|
18
|
+
options[:class] = merge_css_classes("lesli-form-input", options[:class])
|
|
19
|
+
super(method, options)
|
|
16
20
|
end
|
|
17
21
|
|
|
18
22
|
def text_editor(method, options = {})
|
|
@@ -26,22 +30,30 @@ module LesliView
|
|
|
26
30
|
|
|
27
31
|
|
|
28
32
|
def email_field(method, options = {})
|
|
29
|
-
|
|
30
|
-
|
|
33
|
+
options = options.symbolize_keys
|
|
34
|
+
options[:class] = merge_css_classes("lesli-form-input", options[:class])
|
|
35
|
+
super(method, options)
|
|
31
36
|
end
|
|
32
37
|
|
|
33
38
|
def password_field(method, options = {})
|
|
34
|
-
|
|
35
|
-
|
|
39
|
+
options = options.symbolize_keys
|
|
40
|
+
options[:class] = merge_css_classes("lesli-form-input", options[:class])
|
|
41
|
+
options[:value] = @object.public_send(method) if @object && !options.key?(:value)
|
|
42
|
+
super(method, options)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def select(method, choices = nil, options = {}, html_options = {}, &block)
|
|
46
|
+
html_options = html_options.symbolize_keys
|
|
47
|
+
html_options[:class] = merge_css_classes("lesli-form-select", html_options[:class])
|
|
48
|
+
super(method, choices, options, html_options, &block)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def submit(value = nil, options = {})
|
|
52
|
+
options = options.symbolize_keys
|
|
53
|
+
options[:class] = merge_css_classes("lesli-form-button is-primary", options[:class])
|
|
54
|
+
super(value, options)
|
|
36
55
|
end
|
|
37
56
|
|
|
38
|
-
def submit(value=nil, options = {})
|
|
39
|
-
# Extract and merge classes properly
|
|
40
|
-
default_classes = "button is-primary"
|
|
41
|
-
custom_classes = options[:class] || ""
|
|
42
|
-
merged_classes = "#{default_classes} #{custom_classes}".strip
|
|
43
|
-
super(value, options.merge(class: merged_classes))
|
|
44
|
-
end
|
|
45
57
|
end
|
|
46
58
|
end
|
|
47
59
|
end
|
|
@@ -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>
|
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>
|