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.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/lib/lesli_view/charts/bar.rb +10 -0
  3. data/lib/lesli_view/charts/general.html.erb +38 -52
  4. data/lib/lesli_view/charts/general.rb +69 -30
  5. data/lib/lesli_view/charts/line.rb +16 -0
  6. data/lib/lesli_view/components/header.html.erb +48 -23
  7. data/lib/lesli_view/components/header.rb +35 -2
  8. data/lib/lesli_view/components/tabs.html.erb +59 -26
  9. data/lib/lesli_view/components/tabs.rb +7 -7
  10. data/lib/lesli_view/components/timeline.html.erb +28 -25
  11. data/lib/lesli_view/components/toolbar.css +52 -0
  12. data/lib/lesli_view/components/toolbar.html.erb +36 -21
  13. data/lib/lesli_view/components/toolbar.rb +41 -4
  14. data/lib/lesli_view/elements/avatar.html.erb +3 -3
  15. data/lib/lesli_view/elements/avatar.rb +8 -8
  16. data/lib/lesli_view/elements/button.css +357 -0
  17. data/lib/lesli_view/elements/button.html.erb +15 -12
  18. data/lib/lesli_view/elements/button.rb +74 -34
  19. data/lib/lesli_view/elements/empty.html.erb +31 -6
  20. data/lib/lesli_view/elements/empty.rb +8 -1
  21. data/lib/lesli_view/elements/table-chart.html.erb +42 -0
  22. data/lib/lesli_view/elements/table.css +91 -0
  23. data/lib/lesli_view/elements/table.html.erb +24 -21
  24. data/lib/lesli_view/elements/table.rb +67 -17
  25. data/lib/lesli_view/forms/builder.rb +16 -3
  26. data/lib/lesli_view/forms/builder_horizontal.rb +34 -10
  27. data/lib/lesli_view/forms/fields.rb +103 -70
  28. data/lib/lesli_view/forms/fieldset.rb +3 -3
  29. data/lib/lesli_view/forms/form.css +171 -0
  30. data/lib/lesli_view/forms/form.scss +2 -54
  31. data/lib/lesli_view/forms/inputs.rb +27 -15
  32. data/lib/lesli_view/items/activities.html.erb +4 -6
  33. data/lib/lesli_view/items/discussion.html.erb +6 -5
  34. data/lib/lesli_view/items/discussions.html.erb +8 -6
  35. data/lib/lesli_view/items/task.html.erb +16 -11
  36. data/lib/lesli_view/items/tasks.html.erb +8 -13
  37. data/lib/lesli_view/layout/container.html.erb +8 -2
  38. data/lib/lesli_view/partials/engine_spec.html.erb +28 -41
  39. data/lib/lesli_view/version.rb +2 -2
  40. data/lib/lesli_view/widgets/calendar.html.erb +42 -133
  41. data/lib/lesli_view/widgets/calendar.rb +77 -3
  42. data/lib/lesli_view/widgets/chart.html.erb +25 -26
  43. data/lib/lesli_view/widgets/chart.rb +46 -3
  44. data/lib/lesli_view/widgets/count.html.erb +17 -15
  45. data/lib/lesli_view/widgets/count.rb +13 -1
  46. data/lib/lesli_view/widgets/date.html.erb +23 -21
  47. data/lib/lesli_view/widgets/date.rb +27 -2
  48. data/lib/lesli_view/widgets/weather.html.erb +40 -38
  49. data/lib/lesli_view/widgets/weather.rb +41 -2
  50. data/readme.md +185 -29
  51. metadata +6 -10
  52. data/lib/lesli_view/components/header.scss +0 -69
  53. data/lib/lesli_view/components/tabs.scss +0 -49
  54. data/lib/lesli_view/components/tabs_spec.rb +0 -0
  55. data/lib/lesli_view/components/timeline.scss +0 -216
  56. data/lib/lesli_view/components/toolbar.scss +0 -54
  57. data/lib/lesli_view/elements/avatar.scss +0 -50
  58. data/lib/lesli_view/elements/table.scss +0 -153
  59. data/lib/lesli_view/items/activities.scss +0 -39
  60. data/lib/lesli_view/items/discussions.scss +0 -47
@@ -1,25 +1,40 @@
1
- <section class="lesli-toolbar mb-5">
2
- <div class="field is-grouped">
3
- <div class="control is-expanded has-icons-right has-icons-left">
4
- <%= text_field_tag(:search, initial_value,
5
- placeholder: search_placeholder,
6
- class: "input is-shadowless",
7
- data: { action: "input->toolbar#search" }
8
- )%>
9
- <span class="icon is-small is-left has-text-grey-light">
10
- <span class="material-symbols">
11
- search
12
- </span>
13
- </span>
14
- <span class="icon is-right">
15
- <a class="delete" data-action="click->toolbar#clearSearch"></a>
16
- </span>
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
- <%= content %>
19
- <% if pagination %>
20
- <div class="select">
21
- <%= select_tag :pagination, options_for_select([10, 20]), class: "select" %>
22
+
23
+ <% if filters? %>
24
+ <div class="lesli-toolbar__filters">
25
+ <%= filters %>
22
26
  </div>
23
27
  <% end %>
24
- </div>
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
- attr_reader :search_placeholder, :initial_value, :pagination
39
-
40
- def initialize(search_placeholder = "Search...", initial_value: nil, pagination: true)
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
- @pagination = pagination
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
@@ -1,7 +1,7 @@
1
- <figure class="lesli-element-avatar has-background-grey-lighter" style="<%= avatar_style %>">
1
+ <figure class="lesli-element-avatar <%= avatar_size %> flex shrink-0 items-center justify-center overflow-hidden rounded-full border-[0.1px] border-slate-400 bg-slate-100">
2
2
  <% if image %>
3
- <img src="<%= image %>" alt="Avatar image">
3
+ <img class="h-full w-full object-cover" src="<%= image %>" alt="Avatar image">
4
4
  <% else %>
5
- <span class="has-text-weight-bold <%= font_size %>"><%= letter %></span>
5
+ <span class="font-bold text-slate-700 <%= font_size %>"><%= letter %></span>
6
6
  <% end %>
7
7
  </figure>
@@ -32,7 +32,7 @@ Building a better future, one line of code at a time.
32
32
  module LesliView
33
33
  module Elements
34
34
  class Avatar < ViewComponent::Base
35
- attr_reader :image, :name, :size, :letter, :font_size, :avatar_style
35
+ attr_reader :image, :name, :size, :letter, :font_size, :avatar_size
36
36
 
37
37
  def initialize(image: nil, name: "", size: "medium")
38
38
  @image = image
@@ -40,7 +40,7 @@ module LesliView
40
40
  @size = size
41
41
  @letter = ""
42
42
  @font_size = ""
43
- @avatar_style = ""
43
+ @avatar_size = ""
44
44
  calculate_size
45
45
  get_letter unless @image
46
46
  end
@@ -51,14 +51,14 @@ module LesliView
51
51
  def calculate_size
52
52
  case size
53
53
  when "small"
54
- @font_size = "is-size-5"
55
- @avatar_style = "height: 60px; width: 60px;"
54
+ @font_size = "text-xl"
55
+ @avatar_size = "size-[60px]"
56
56
  when "medium"
57
- @font_size = "is-size-3"
58
- @avatar_style = "height: 120px; width: 120px;"
57
+ @font_size = "text-[2rem]"
58
+ @avatar_size = "size-[120px]"
59
59
  when "large"
60
- @font_size = "is-size-1"
61
- @avatar_style = "height: 180px; width: 180px;"
60
+ @font_size = "text-5xl"
61
+ @avatar_size = "size-[180px]"
62
62
  else
63
63
  raise ArgumentError, "Invalid size: #{size}"
64
64
  end
@@ -0,0 +1,357 @@
1
+ /*!
2
+
3
+ Lesli
4
+
5
+ Copyright (c) 2026, Lesli Technologies, S. A.
6
+
7
+ This program is free software: you can redistribute it and/or modify
8
+ it under the terms of the GNU General Public License as published by
9
+ the Free Software Foundation, either version 3 of the License, or
10
+ (at your option) any later version.
11
+
12
+ This program is distributed in the hope that it will be useful,
13
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ GNU General Public License for more details.
16
+
17
+ You should have received a copy of the GNU General Public License
18
+ along with this program. If not, see http://www.gnu.org/licenses/.
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
+
33
+
34
+ @layer components {
35
+ .lesli-button:hover {
36
+ cursor: pointer;
37
+ }
38
+ .lesli-button {
39
+ @apply
40
+ cursor-pointer
41
+ inline-flex
42
+ items-center
43
+ justify-center
44
+ gap-2
45
+ border
46
+ rounded-lg
47
+ h-10
48
+ px-4
49
+ text-sm
50
+ font-medium
51
+ whitespace-nowrap
52
+ transition-colors
53
+ duration-150
54
+ focus:outline-none
55
+ focus-visible:ring-2
56
+ focus-visible:ring-offset-2
57
+ disabled:pointer-events-none
58
+ disabled:opacity-50;
59
+ }
60
+
61
+ .lesli-button.is-small {
62
+ @apply
63
+ h-8
64
+ rounded-md
65
+ px-3
66
+ text-xs;
67
+ }
68
+
69
+ .lesli-button.is-icon-only {
70
+ @apply
71
+ w-10
72
+ px-0;
73
+ }
74
+
75
+ .lesli-button.is-icon-only.is-small {
76
+ @apply
77
+ w-8
78
+ px-0;
79
+ }
80
+
81
+ .lesli-button.is-loading {
82
+ @apply
83
+ pointer-events-none
84
+ cursor-wait
85
+ opacity-80;
86
+ }
87
+
88
+ .lesli-button.is-loading::before {
89
+ width: 1rem;
90
+ height: 1rem;
91
+ content: "";
92
+ border: 2px solid currentColor;
93
+ border-right-color: transparent;
94
+ border-radius: 9999px;
95
+ animation: lesli-button-spin .65s linear infinite;
96
+ }
97
+
98
+ /*
99
+ * Solid variants
100
+ */
101
+
102
+ .lesli-button.is-primary {
103
+ @apply
104
+ border-blue-600
105
+ bg-blue-600
106
+ text-white
107
+ hover:border-blue-700
108
+ hover:bg-blue-700
109
+ focus-visible:ring-blue-500;
110
+ }
111
+
112
+ .lesli-button.is-info {
113
+ @apply
114
+ border-sky-600
115
+ bg-sky-600
116
+ text-white
117
+ hover:border-sky-700
118
+ hover:bg-sky-700
119
+ focus-visible:ring-sky-500;
120
+ }
121
+
122
+ .lesli-button.is-success {
123
+ @apply
124
+ border-emerald-600
125
+ bg-emerald-600
126
+ text-white
127
+ hover:border-emerald-700
128
+ hover:bg-emerald-700
129
+ focus-visible:ring-emerald-500;
130
+ }
131
+
132
+ .lesli-button.is-warning {
133
+ @apply
134
+ border-amber-500
135
+ bg-amber-500
136
+ text-amber-950
137
+ hover:border-amber-600
138
+ hover:bg-amber-600
139
+ focus-visible:ring-amber-500;
140
+ }
141
+
142
+ .lesli-button.is-danger {
143
+ @apply
144
+ border-rose-600
145
+ bg-rose-600
146
+ text-white
147
+ hover:border-rose-700
148
+ hover:bg-rose-700
149
+ focus-visible:ring-rose-500;
150
+ }
151
+
152
+ /*
153
+ * Light variants
154
+ *
155
+ * These must come after solid variants.
156
+ */
157
+
158
+ .lesli-button.is-primary.is-light {
159
+ @apply
160
+ border-blue-100
161
+ bg-blue-50
162
+ text-blue-700
163
+ hover:border-blue-200
164
+ hover:bg-blue-100
165
+ focus-visible:ring-blue-500;
166
+ }
167
+
168
+ .lesli-button.is-info.is-light {
169
+ @apply
170
+ border-sky-100
171
+ bg-sky-50
172
+ text-sky-700
173
+ hover:border-sky-200
174
+ hover:bg-sky-100
175
+ focus-visible:ring-sky-500;
176
+ }
177
+
178
+ .lesli-button.is-success.is-light {
179
+ @apply
180
+ border-emerald-100
181
+ bg-emerald-50
182
+ text-emerald-700
183
+ hover:border-emerald-200
184
+ hover:bg-emerald-100
185
+ focus-visible:ring-emerald-500;
186
+ }
187
+
188
+ .lesli-button.is-warning.is-light {
189
+ @apply
190
+ border-amber-100
191
+ bg-amber-50
192
+ text-amber-700
193
+ hover:border-amber-200
194
+ hover:bg-amber-100
195
+ focus-visible:ring-amber-500;
196
+ }
197
+
198
+ .lesli-button.is-danger.is-light {
199
+ @apply
200
+ border-rose-100
201
+ bg-rose-50
202
+ text-rose-700
203
+ hover:border-rose-200
204
+ hover:bg-rose-100
205
+ focus-visible:ring-rose-500;
206
+ }
207
+
208
+ /*
209
+ * Outlined variants
210
+ *
211
+ * These must come after solid and light variants.
212
+ */
213
+
214
+ .lesli-button.is-primary.is-outlined {
215
+ @apply
216
+ border-blue-300
217
+ bg-transparent
218
+ text-blue-700
219
+ hover:border-blue-400
220
+ hover:bg-blue-50
221
+ focus-visible:ring-blue-500;
222
+ }
223
+
224
+ .lesli-button.is-info.is-outlined {
225
+ @apply
226
+ border-sky-300
227
+ bg-transparent
228
+ text-sky-700
229
+ hover:border-sky-400
230
+ hover:bg-sky-50
231
+ focus-visible:ring-sky-500;
232
+ }
233
+
234
+ .lesli-button.is-success.is-outlined {
235
+ @apply
236
+ border-emerald-300
237
+ bg-transparent
238
+ text-emerald-700
239
+ hover:border-emerald-400
240
+ hover:bg-emerald-50
241
+ focus-visible:ring-emerald-500;
242
+ }
243
+
244
+ .lesli-button.is-warning.is-outlined {
245
+ @apply
246
+ border-amber-300
247
+ bg-transparent
248
+ text-amber-700
249
+ hover:border-amber-400
250
+ hover:bg-amber-50
251
+ focus-visible:ring-amber-500;
252
+ }
253
+
254
+ .lesli-button.is-danger.is-outlined {
255
+ @apply
256
+ border-rose-300
257
+ bg-transparent
258
+ text-rose-700
259
+ hover:border-rose-400
260
+ hover:bg-rose-50
261
+ focus-visible:ring-rose-500;
262
+ }
263
+
264
+ /*
265
+ * Optional: light + outlined
266
+ */
267
+
268
+ .lesli-button.is-light.is-outlined {
269
+ @apply
270
+ bg-transparent;
271
+ }
272
+ }
273
+
274
+ @keyframes lesli-button-spin {
275
+ to {
276
+ transform: rotate(360deg);
277
+ }
278
+ }
279
+
280
+
281
+
282
+ @layer components {
283
+ .lesli-icon {
284
+ @apply
285
+ inline-flex
286
+ items-center
287
+ justify-center
288
+ shrink-0
289
+ h-6
290
+ w-6
291
+ text-[1.5rem]
292
+ leading-none;
293
+ }
294
+
295
+ .lesli-icon.is-small {
296
+ @apply
297
+ h-4
298
+ w-4
299
+ text-[1rem];
300
+ }
301
+
302
+ .lesli-icon.is-medium {
303
+ @apply
304
+ h-8
305
+ w-8
306
+ text-[2rem];
307
+ }
308
+
309
+ .lesli-icon.is-large {
310
+ @apply
311
+ h-12
312
+ w-12
313
+ text-[3rem];
314
+ }
315
+
316
+ .lesli-icon .material-symbols,
317
+ .lesli-icon .material-symbols-outlined,
318
+ .lesli-icon .material-symbols-rounded,
319
+ .lesli-icon .material-symbols-sharp {
320
+ @apply
321
+ block
322
+ text-inherit
323
+ leading-none;
324
+ }
325
+
326
+ /*
327
+ * Button-specific icon sizing.
328
+ * This keeps icons visually balanced inside .lesli-button.
329
+ */
330
+ .lesli-button .lesli-icon {
331
+ @apply
332
+ h-5
333
+ w-5
334
+ text-[1.25rem];
335
+ }
336
+
337
+ .lesli-button.is-small .lesli-icon {
338
+ @apply
339
+ h-4
340
+ w-4
341
+ text-[1rem];
342
+ }
343
+
344
+ .lesli-button.is-icon-only .lesli-icon {
345
+ @apply
346
+ h-5
347
+ w-5
348
+ text-[1.25rem];
349
+ }
350
+
351
+ .lesli-button.is-icon-only.is-small .lesli-icon {
352
+ @apply
353
+ h-4
354
+ w-4
355
+ text-[1rem];
356
+ }
357
+ }
@@ -6,29 +6,32 @@
6
6
  @url,
7
7
  method: @method,
8
8
  params: @params,
9
- class: html_classes,
10
- data: { turbo_frame: "_top" },
11
- **alpine_attributes) do %>
9
+ disabled: disabled?,
10
+ **common_attributes
11
+ ) do %>
12
12
  <%= button_content %>
13
13
  <% end %>
14
14
 
15
15
  <% elsif mode == :link %>
16
16
 
17
- <%= link_to(
18
- @url,
19
- class: html_classes,
20
- data: { turbo_frame: "_top" },
21
- **alpine_attributes) do %>
22
- <%= button_content %>
17
+ <% if disabled? %>
18
+ <%= content_tag(:span, role: "link", tabindex: -1, **common_attributes) do %>
19
+ <%= button_content %>
20
+ <% end %>
21
+ <% else %>
22
+ <%= link_to(@url, **common_attributes) do %>
23
+ <%= button_content %>
24
+ <% end %>
23
25
  <% end %>
24
26
 
25
27
  <% else %>
26
28
 
27
29
  <%= content_tag(
28
30
  :button,
29
- type: "button",
30
- class: html_classes,
31
- **alpine_attributes) do %>
31
+ type: button_type,
32
+ disabled: disabled?,
33
+ **common_attributes
34
+ ) do %>
32
35
  <%= button_content %>
33
36
  <% end %>
34
37