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
@@ -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 is-flex mt-3 mb-3 <%= "lesli-items-tasks-task-done" if @resource.done %>">
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 type: "submit", disabled: @resource.done? do %>
44
- <p class="icon-text">
45
- <span class="icon">
46
- <span class="material-symbols-outlined">
47
- <%= @resource.done ? "check_box" : "check_box_outline_blank" %>
48
- </span>
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><%= @resource.title %></span>
51
- </p>
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
- <style>
33
- .lesli-items-tasks {
34
- .lesli-items-tasks-tasks-task-done p {
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 id="<%="#{scope_key}-lesli-items-tasks-list"%>">
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
- <!-- app/components/lesli_application_container_component.html.erb -->
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="lesli-application-container container <%= 'is-fluid' if dashboard %>">
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-3 pb-2">
13
- <h5 class="is-title is-size-5 has-text-weight-semibold"><%= engine[:name] %></h5>
14
- <p class="is-size-5"><%= engine[:summary] %></p>
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
- <ul>
17
- <li class="mb-2">
18
- <span class="icon-text">
19
- <span class="material-symbols md-24">
20
- pest_control
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
- <li class="mb-2">
27
- <span class="icon-text">
28
- <span class="material-symbols md-24">
29
- schedule
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
- <li>
36
- <span class="icon-text">
37
- <span class="material-symbols md-24">
38
- library_books
39
- </span>
40
- <span></span>
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>
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LesliView
4
- VERSION = "1.1.2"
5
- BUILD = "1777174912"
4
+ VERSION = "1.1.4"
5
+ BUILD = ""
6
6
  end
@@ -1,135 +1,44 @@
1
- <style>
2
- .lesli-view-widgets-calendar .box {
3
- padding: 14px;
4
- }
5
-
6
- .lesli-view-widgets-calendar-content .weekday,
7
- .lesli-view-widgets-calendar-content .days {
8
- display: grid;
9
- grid-template-columns: repeat(7, 1fr);
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
- const year = date.getFullYear();
86
- const month = date.getMonth();
87
-
88
- // Monday-based week
89
- const firstDay = new Date(year, month, 1);
90
-
91
- const start = firstDay.getDay(); // 0 = Sunday, 6 = Saturday
92
-
93
- const daysInMonth = new Date(year, month + 1, 0).getDate();
94
-
95
- // Empty cells before first day
96
- for (let i = 0; i < start; i++) {
97
- daysEl.appendChild(document.createElement('li'));
98
- }
99
-
100
- // Days
101
- for (let day = 1; day <= daysInMonth; day++) {
102
- const li = document.createElement('li');
103
- const a = document.createElement('a');
104
-
105
- const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
106
-
107
- a.textContent = day;
108
- a.href = '#';
109
- a.dataset.value = day;
110
-
111
- if (events.has(dateStr)) a.classList.add('event');
112
-
113
- if (
114
- day === new Date().getDate() &&
115
- month === new Date().getMonth() &&
116
- year === new Date().getFullYear()
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
- attr_reader :title, :number
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(title=nil, number=nil)
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
- <style>
2
- .lesli-view-widgets-chart {
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
- # Determine the chart component class dynamically
8
- chart_component = case type
9
- when :bar
10
- LesliView::Charts::Bar
11
- when :line
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
- <div class="lesli-view-widgets-chart">
19
- <% if chart_component %>
20
- <%= render(chart_component.new(
21
- title: title,
22
- labels: data.map{ |i| i[0] },
23
- dataset: data.map{ |i| i[1] },
24
- height: '132px',
25
- compact: true
26
- )) %>
27
- <% end %>
28
- </div>
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
- <style>
2
- .lesli-view-widgets-count p {
3
- font-size: 4rem;
4
- font-weight: 600;
5
- }
6
- </style>
7
- <div class="lesli-view-widgets-count">
8
- <div class="box">
9
- <h6 class="is-title is-size-6 mb-0">
10
- <%= title %>
11
- </h6>
12
- <p class="has-text-centered">
13
- <%= number %>
14
- </p>
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
- </div>
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