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
@@ -35,33 +35,38 @@ Building a better future, one line of code at a time.
35
35
  module LesliView
36
36
  module Elements
37
37
  class Button < ViewComponent::Base
38
- attr_reader :label, :url, :icon, :loading, :solid, :info, :success, :warning, :danger, :small, :dispatch, :method, :params, :css_class
38
+ attr_reader :label, :url, :icon, :loading, :solid, :light,
39
+ :info, :success, :warning, :danger, :small, :dispatch,
40
+ :method, :params, :css_class, :aria_label, :disabled,
41
+ :button_type, :data
39
42
 
40
- # Adds two numbers together.
41
- # @param [Integer] a The first number.
42
- # @param [Integer] b The second number.
43
- # @return [Integer] The sum of the two numbers.
44
43
  def initialize(
45
- label=nil,
46
- url:nil,
47
- icon: nil,
48
- loading: false,
49
- solid: false,
50
- info: false,
51
- success: false,
52
- warning: false,
53
- danger: false,
44
+ label = nil,
45
+ url: nil,
46
+ icon: nil,
47
+ loading: false,
48
+ solid: true,
49
+ light: false,
50
+ info: true,
51
+ success: false,
52
+ warning: false,
53
+ danger: false,
54
54
  small: false,
55
- dispatch:nil,
55
+ dispatch: nil,
56
56
  method: nil,
57
- params:nil,
58
- css_class:nil
57
+ params: nil,
58
+ css_class: nil,
59
+ aria_label: nil,
60
+ disabled: false,
61
+ type: :button,
62
+ data: {}
59
63
  )
60
64
  @label = label
61
65
  @url = url
62
66
  @icon = icon
63
67
  @loading = loading
64
68
  @solid = solid
69
+ @light = light
65
70
  @info = info
66
71
  @success = success
67
72
  @warning = warning
@@ -71,6 +76,10 @@ module LesliView
71
76
  @method = method
72
77
  @params = params
73
78
  @css_class = css_class
79
+ @aria_label = aria_label
80
+ @disabled = disabled
81
+ @button_type = type
82
+ @data = data
74
83
  end
75
84
 
76
85
  def mode
@@ -80,20 +89,22 @@ module LesliView
80
89
  end
81
90
 
82
91
  def alpine_attributes
83
- return {} unless @dispatch
92
+ return {} unless dispatch.present?
84
93
 
85
94
  {
86
- "x-data": "",
87
- "@click": "$dispatch('#{@dispatch}')"
95
+ "x-data": "",
96
+ "@click": "$dispatch('#{dispatch}')"
88
97
  }
89
98
  end
90
99
 
91
100
  def html_classes
92
- classes = ["button", button_variant]
93
- classes << "is-light" << "is-outlined" unless solid
101
+ classes = ["lesli-button", button_variant]
102
+ classes << "is-outlined" unless solid
103
+ classes << "is-light" if light
94
104
  classes << "is-loading" if loading
95
105
  classes << "is-small" if small
96
- classes << css_class
106
+ classes << "is-icon-only" if icon_only?
107
+ classes << css_class if css_class.present?
97
108
  classes.join(" ")
98
109
  end
99
110
 
@@ -106,21 +117,50 @@ module LesliView
106
117
  end
107
118
 
108
119
  def icon_only?
109
- icon && !label
120
+ icon.present? && visible_label.blank?
121
+ end
122
+
123
+ def disabled?
124
+ disabled || loading
125
+ end
126
+
127
+ def visible_label
128
+ label.presence || (content if content?)
129
+ end
130
+
131
+ def accessible_label
132
+ aria_label.presence || (icon.to_s.humanize if icon_only?)
133
+ end
134
+
135
+ def html_data
136
+ { turbo_frame: "_top" }.merge(data || {})
137
+ end
138
+
139
+ def common_attributes
140
+ attributes = {
141
+ class: html_classes,
142
+ data: html_data,
143
+ aria: {
144
+ label: accessible_label,
145
+ busy: loading || nil,
146
+ disabled: disabled? || nil
147
+ }.compact
148
+ }
149
+
150
+ attributes.merge(alpine_attributes)
110
151
  end
111
152
 
112
153
  def button_content
113
- if @icon
114
- safe_join([
115
- content_tag(:span,
116
- content_tag(:span, @icon, class: "material-symbols"),
117
- class: "icon #{'is-small' if @small}"
118
- ),
119
- (@icon_only ? nil : content_tag(:span, @label))
120
- ].compact)
121
- else
122
- @label
154
+ parts = []
155
+
156
+ if icon.present?
157
+ parts << content_tag(:span, class: "lesli-icon", aria: { hidden: true }) do
158
+ content_tag(:span, icon, class: "material-symbols")
159
+ end
123
160
  end
161
+
162
+ parts << content_tag(:span, visible_label) if visible_label.present?
163
+ safe_join(parts)
124
164
  end
125
165
  end
126
166
  end
@@ -1,8 +1,33 @@
1
1
 
2
- <div class="lesli-empty has-text-centered">
3
- <svg width="184" height="184" viewBox="0 0 184 184" fill="none" xmlns="http://www.w3.org/2000/svg">
4
- <path d="M145.271 90.6522H142.735V89.528C142.735 83.9793 138.222 79.4655 132.673 79.4655H85.7357C84.255 79.4655 82.1132 78.0942 81.4922 76.7515L76.1878 65.2544C74.1523 60.8499 68.9629 57.5293 64.1128 57.5293H39.7012C34.1524 57.5293 29.6387 62.043 29.6387 67.5918V151.809C29.6387 157.358 34.1524 161.872 39.7012 161.872H53.3459C53.5357 161.883 53.7139 161.932 53.9065 161.932H112.945V153.307H108.831H53.9065C53.7197 153.307 53.6219 153.278 53.5587 153.247C53.527 153.232 53.4897 153.215 53.481 153.206C53.458 153.181 53.412 153.045 53.4609 152.772L63.1985 101.419C63.4027 100.338 64.6849 99.2772 65.7803 99.2772H134.11H142.735H145.271C145.553 99.2772 145.679 99.349 145.697 99.3749C145.714 99.4008 145.766 99.533 145.714 99.809L142.735 115.524L135.979 151.159C135.775 152.24 134.493 153.301 133.395 153.301H121.964V161.926H133.395C138.621 161.926 143.48 157.904 144.455 152.767L154.189 101.413C154.713 98.6648 154.037 95.9163 152.341 93.8693C150.645 91.8281 148.069 90.6522 145.271 90.6522ZM134.11 90.6522H65.7832C60.5593 90.6522 55.7005 94.6743 54.7259 99.8119L44.9883 151.168C44.856 151.864 44.8043 152.56 44.8244 153.247H39.7012C38.922 153.247 38.2637 152.588 38.2637 151.809V67.5918C38.2637 66.8127 38.922 66.1543 39.7012 66.1543H64.1128C65.5934 66.1543 67.7353 67.5257 68.3563 68.8683L73.6607 80.3654C75.6962 84.7699 80.8855 88.0905 85.7357 88.0905H132.673C133.452 88.0905 134.11 88.7489 134.11 89.528V90.6522Z" fill="#BDBDBD"/>
5
- <path d="M145.7 50.5227L153.201 43.0219C154.805 41.4176 154.739 38.7525 153.054 37.0677C151.369 35.383 148.704 35.3197 147.1 36.9211L139.599 44.422L132.098 36.9211C130.494 35.3169 127.829 35.383 126.144 37.0677C124.459 38.7525 124.396 41.4176 125.997 43.0219L133.498 50.5227L125.997 58.0236C124.393 59.6279 124.459 62.293 126.144 63.9777C127.829 65.6625 130.494 65.7257 132.098 64.1244L139.599 56.6235L147.1 64.1244C148.704 65.7286 151.369 65.6625 153.054 63.9777C154.739 62.293 154.802 59.6279 153.201 58.0236L145.7 50.5227Z" fill="#BDBDBD"/>
6
- </svg>
7
- <p class="has-text-grey"><%= @text %></p>
2
+ <div
3
+ class="lesli-empty-state flex w-full flex-col items-center justify-center px-6 py-10 text-center sm:py-12"
4
+ role="region"
5
+ aria-label="<%= text.presence || "Empty state" %>">
6
+
7
+ <div class="mb-5 flex h-36 w-36 items-center justify-center rounded-full bg-slate-100/80 sm:h-40 sm:w-40">
8
+ <svg
9
+ class="h-28 w-28 text-slate-300 sm:h-32 sm:w-32"
10
+ viewBox="0 0 184 184"
11
+ fill="none"
12
+ xmlns="http://www.w3.org/2000/svg"
13
+ aria-hidden="true"
14
+ focusable="false">
15
+ <path d="M145.271 90.6522H142.735V89.528C142.735 83.9793 138.222 79.4655 132.673 79.4655H85.7357C84.255 79.4655 82.1132 78.0942 81.4922 76.7515L76.1878 65.2544C74.1523 60.8499 68.9629 57.5293 64.1128 57.5293H39.7012C34.1524 57.5293 29.6387 62.043 29.6387 67.5918V151.809C29.6387 157.358 34.1524 161.872 39.7012 161.872H53.3459C53.5357 161.883 53.7139 161.932 53.9065 161.932H112.945V153.307H108.831H53.9065C53.7197 153.307 53.6219 153.278 53.5587 153.247C53.527 153.232 53.4897 153.215 53.481 153.206C53.458 153.181 53.412 153.045 53.4609 152.772L63.1985 101.419C63.4027 100.338 64.6849 99.2772 65.7803 99.2772H134.11H142.735H145.271C145.553 99.2772 145.679 99.349 145.697 99.3749C145.714 99.4008 145.766 99.533 145.714 99.809L142.735 115.524L135.979 151.159C135.775 152.24 134.493 153.301 133.395 153.301H121.964V161.926H133.395C138.621 161.926 143.48 157.904 144.455 152.767L154.189 101.413C154.713 98.6648 154.037 95.9163 152.341 93.8693C150.645 91.8281 148.069 90.6522 145.271 90.6522ZM134.11 90.6522H65.7832C60.5593 90.6522 55.7005 94.6743 54.7259 99.8119L44.9883 151.168C44.856 151.864 44.8043 152.56 44.8244 153.247H39.7012C38.922 153.247 38.2637 152.588 38.2637 151.809V67.5918C38.2637 66.8127 38.922 66.1543 39.7012 66.1543H64.1128C65.5934 66.1543 67.7353 67.5257 68.3563 68.8683L73.6607 80.3654C75.6962 84.7699 80.8855 88.0905 85.7357 88.0905H132.673C133.452 88.0905 134.11 88.7489 134.11 89.528V90.6522Z" fill="currentColor"/>
16
+ <path d="M145.7 50.5227L153.201 43.0219C154.805 41.4176 154.739 38.7525 153.054 37.0677C151.369 35.383 148.704 35.3197 147.1 36.9211L139.599 44.422L132.098 36.9211C130.494 35.3169 127.829 35.383 126.144 37.0677C124.459 38.7525 124.396 41.4176 125.997 43.0219L133.498 50.5227L125.997 58.0236C124.393 59.6279 124.459 62.293 126.144 63.9777C127.829 65.6625 130.494 65.7257 132.098 64.1244L139.599 56.6235L147.1 64.1244C148.704 65.7286 151.369 65.6625 153.054 63.9777C154.739 62.293 154.802 59.6279 153.201 58.0236L145.7 50.5227Z" fill="currentColor"/>
17
+ </svg>
18
+ </div>
19
+
20
+ <% if text.present? %>
21
+ <p class="text-base font-semibold text-slate-700 sm:text-lg"><%= text %></p>
22
+ <% end %>
23
+
24
+ <% if description.present? %>
25
+ <p class="mt-2 max-w-md text-sm leading-6 text-slate-500"><%= description %></p>
26
+ <% end %>
27
+
28
+ <% if content? %>
29
+ <div class="mt-6 flex flex-wrap items-center justify-center gap-3">
30
+ <%= content %>
31
+ </div>
32
+ <% end %>
8
33
  </div>
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  =begin
2
4
 
3
5
  Lesli
@@ -32,8 +34,13 @@ Building a better future, one line of code at a time.
32
34
  module LesliView
33
35
  module Elements
34
36
  class Empty < ViewComponent::Base
35
- def initialize(text: "No data found")
37
+ attr_reader :text, :description
38
+
39
+ # `text` remains optional for icon-only uses such as error pages.
40
+ # A component block can be supplied to render an action below the message.
41
+ def initialize(text: "No data found", description: nil)
36
42
  @text = text
43
+ @description = description
37
44
  end
38
45
  end
39
46
  end
@@ -0,0 +1,42 @@
1
+ distribution: [{
2
+ label: "Deactivated_users",
3
+ value: summary[:total_users_deactivated],
4
+ percentage: percentage(summary[:total_users_deactivated], summary[:total_users]),
5
+ color: "#5D7C4D"
6
+ }, {
7
+ label: "Locked_users",
8
+ value: summary[:total_users_locked],
9
+ percentage: percentage(summary[:total_users_locked], summary[:total_users]),
10
+ color: "#B23A3A"
11
+ }, {
12
+ label: "Unconfirmed_users",
13
+ value: summary[:total_users_unconfirmed],
14
+ percentage: percentage(summary[:total_users_unconfirmed], summary[:total_users]),
15
+ color: "#5F6C7D"
16
+ }, {
17
+ label: "Deleted_users",
18
+ value: summary[:total_users_deleted],
19
+ percentage: percentage(summary[:total_users_deleted], summary[:total_users]),
20
+ color: "#4B5FA8"
21
+ }],
22
+ <%= render(LesliView::Elements::Table.new(columns: [])) do |table| %>
23
+ <% @users[:distribution].each do |user| %>
24
+ <% table.with_row do |row| %>
25
+ <% row.with_cell do %>
26
+ <%= user[:label] %>
27
+ <% end %>
28
+ <% row.with_cell(class_name: "w-full p-0") do %>
29
+ <p style="padding:8px;border-radius:4px;background:<%= user[:color] %>;width:<%= user[:percentage] %>%">
30
+ </p>
31
+ <% end %>
32
+ <% row.with_cell(align: :right) do %>
33
+ <%= user[:value] %>
34
+ <% end %>
35
+ <% row.with_cell(align: :right) do %>
36
+ <span class="font-semibold text-slate-400">
37
+ <%= user[:percentage] %>%
38
+ </span>
39
+ <% end %>
40
+ <% end %>
41
+ <% end %>
42
+ <% end %>
@@ -0,0 +1,91 @@
1
+ /* Tailwind styles for LesliView::Elements::Table. */
2
+
3
+ @layer components {
4
+ .lesli-table-container {
5
+ @apply w-full overflow-x-auto rounded-2xl border border-slate-200 bg-white shadow-sm;
6
+ }
7
+
8
+ .lesli-table {
9
+ width: 100%;
10
+ min-width: 40rem;
11
+ border-collapse: collapse;
12
+ @apply text-left text-sm text-slate-600;
13
+ }
14
+
15
+ .lesli-table thead {
16
+ @apply border-b border-slate-200 bg-slate-50;
17
+ }
18
+
19
+ .lesli-table th {
20
+ @apply whitespace-nowrap px-4 py-3 text-xs font-semibold uppercase tracking-wide text-slate-600;
21
+ }
22
+
23
+ .lesli-table td {
24
+ @apply border-b border-slate-100 px-4 py-3 align-middle;
25
+ }
26
+
27
+ .lesli-table tbody tr {
28
+ @apply transition-colors hover:bg-sky-50/50;
29
+ }
30
+
31
+ .lesli-table tbody tr:last-child td {
32
+ @apply border-b-0;
33
+ }
34
+
35
+ .lesli-table--striped tbody tr:nth-child(even) {
36
+ @apply bg-slate-50/60;
37
+ }
38
+
39
+ .lesli-table--striped tbody tr:nth-child(even):hover {
40
+ @apply bg-sky-50/70;
41
+ }
42
+
43
+ .lesli-table--compact th,
44
+ .lesli-table--compact td {
45
+ @apply px-3 py-2;
46
+ }
47
+
48
+ .lesli-table__cell--center {
49
+ @apply text-center;
50
+ }
51
+
52
+ .lesli-table__cell--right {
53
+ @apply text-right;
54
+ }
55
+
56
+ .lesli-table__cell--left {
57
+ @apply text-left;
58
+ }
59
+
60
+ .lesli-table td.lesli-table__cell--linked {
61
+ @apply p-0;
62
+ }
63
+
64
+ .lesli-table__link {
65
+ @apply block px-4 py-3 text-slate-700 transition-colors hover:text-sky-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-sky-500/40;
66
+ }
67
+
68
+ .lesli-table--compact .lesli-table__link {
69
+ @apply px-3 py-2;
70
+ }
71
+
72
+ .lesli-table__row--danger {
73
+ @apply bg-rose-50 text-rose-900;
74
+ }
75
+
76
+ .lesli-table__row--warning {
77
+ @apply bg-amber-50 text-amber-900;
78
+ }
79
+
80
+ .lesli-table__row--info {
81
+ @apply bg-sky-50 text-sky-900;
82
+ }
83
+
84
+ .lesli-table__row--success {
85
+ @apply bg-emerald-50 text-emerald-900;
86
+ }
87
+
88
+ .lesli-table__empty {
89
+ @apply py-12 text-center text-sm text-slate-400;
90
+ }
91
+ }
@@ -1,24 +1,18 @@
1
- <div class="lesli-table-container table-container">
2
- <table id="<%= id %>" class="table is-fullwidth lesli-table <%= class_name %>">
1
+ <div class="lesli-table-container">
2
+ <table id="<%= id %>" class="<%= table_classes %>">
3
+ <% if caption.present? %>
4
+ <caption class="sr-only"><%= caption %></caption>
5
+ <% end %>
6
+
3
7
  <% unless headless %>
4
8
  <thead>
5
9
  <tr>
6
10
  <% columns.each do |column| %>
7
- <th
8
- width="<%= column[:width] %>"
9
- class="<%= table_header_class(column) %>"
10
- data-action="click->table#sort"
11
- data-field="<%= column[:field] %>">
12
- <% if column[:sort] %>
13
- <span class="icon-text">
14
- <span><%= column[:label] %></span>
15
- <span class="icon">
16
- <span class="material-symbols"><%= column[:field] == @current_sort ? (current_sort_dir(column) == "asc" ? "arrow_upward" : "arrow_downward") : "sort" %></span>
17
- </span>
18
- </span>
19
- <% else %>
20
- <%= column[:label] %>
21
- <% end %>
11
+ <th
12
+ scope="col"
13
+ width="<%= column[:width] %>"
14
+ class="<%= column_class(column) %>">
15
+ <%= column[:label] %>
22
16
  </th>
23
17
  <% end %>
24
18
  </tr>
@@ -26,14 +20,15 @@
26
20
  <% end %>
27
21
  <tbody>
28
22
  <% if records %>
29
- <% records.each_with_index do |record, i| %>
23
+ <% records.each do |record| %>
30
24
  <tr>
31
25
  <% columns.each do |column| %>
32
- <td class="<%= table_body_class(column) %>">
26
+ <% value = value_for(record, column) %>
27
+ <td class="<%= class_names(column_class(column), "lesli-table__cell--linked": link.present?) %>">
33
28
  <% if link %>
34
- <%= link_to(record[column[:field]] || "", link.call(record), class: "link", data: { turbo_frame: '_top' }) %>
29
+ <%= link_to(value || "", link.call(record), class: "lesli-table__link", data: { turbo_frame: "_top" }) %>
35
30
  <% else %>
36
- <%= record[column[:field]] %>
31
+ <%= value %>
37
32
  <% end %>
38
33
  </td>
39
34
  <% end %>
@@ -43,6 +38,14 @@
43
38
  <% rows.each do |row| %>
44
39
  <%= row %>
45
40
  <% end %>
41
+
42
+ <% if empty? %>
43
+ <tr>
44
+ <td class="lesli-table__empty" colspan="<%= [columns.size, 1].max %>">
45
+ <%= empty_text %>
46
+ </td>
47
+ </tr>
48
+ <% end %>
46
49
  </tbody>
47
50
  </table>
48
51
  </div>
@@ -33,50 +33,100 @@ Building a better future, one line of code at a time.
33
33
  module LesliView
34
34
  module Elements
35
35
  class Table < ViewComponent::Base
36
- attr_reader :id, :class_name, :pagination, :loading, :headless, :columns, :records, :link
36
+ attr_reader :id, :class_name, :headless, :columns, :records, :link, :caption, :empty_text
37
37
 
38
38
  renders_many :rows, "TableRow"
39
39
 
40
- def initialize(columns: nil, records: nil, id: nil, class_name: "is-striped", pagination: nil, loading: false, headless: false, link: nil)
40
+ def initialize(
41
+ columns: nil,
42
+ records: nil,
43
+ id: nil,
44
+ class_name: nil,
45
+ headless: false,
46
+ link: nil,
47
+ caption: nil,
48
+ empty: nil,
49
+ striped: true,
50
+ compact: false
51
+ )
41
52
  @id = id
42
53
  @class_name = class_name
43
- @pagination = pagination
44
- @loading = loading
45
54
  @headless = headless
46
- @columns = columns
55
+ @columns = Array(columns).map { |column| column.to_h.symbolize_keys }
47
56
  @records = records
48
57
  @link = link
58
+ @caption = caption
59
+ @empty_text = empty || I18n.t("lesli_view.table.empty", default: "No records found")
60
+ @striped = striped
61
+ @compact = compact
49
62
  end
50
63
 
51
- def table_header_class(column)
52
- column[:field] == "id" || column[:align] == "center" ? "has-text-centered" : ""
64
+ def table_classes
65
+ class_names(
66
+ "lesli-table",
67
+ ("lesli-table--striped" if @striped),
68
+ ("lesli-table--compact" if @compact),
69
+ class_name
70
+ )
53
71
  end
54
72
 
55
- def table_body_class(column)
56
- column[:field] == "id" || column[:align] == "center" ? "has-text-centered" : ""
73
+ def column_class(column)
74
+ alignment = column[:align]&.to_sym
75
+ alignment = :center if column[:field].to_s == "id"
76
+ return unless %i[left center right].include?(alignment)
77
+
78
+ "lesli-table__cell--#{alignment}"
79
+ end
80
+
81
+ def value_for(record, column)
82
+ field = column.fetch(:field)
83
+ field_name = field.to_s
84
+
85
+ if record.respond_to?(:attributes) && record.attributes.key?(field_name)
86
+ record.attributes[field_name]
87
+ elsif record.respond_to?(:key?)
88
+ return record[field] if record.key?(field)
89
+ return record[field_name] if record.key?(field_name)
90
+ return record[field_name.to_sym] if record.key?(field_name.to_sym)
91
+ elsif record.respond_to?(field_name)
92
+ record.public_send(field_name)
93
+ end
94
+ end
95
+
96
+ def empty?
97
+ records.present? ? records.empty? : rows.empty?
57
98
  end
58
99
 
59
100
  class TableRow < ViewComponent::Base
60
101
  renders_many :cells, "TableData"
61
102
 
62
- def initialize(css_class: "")
63
- @css_class = css_class
103
+ STATUSES = %w[danger info success warning].freeze
104
+
105
+ def initialize(class_name: nil, status: nil)
106
+ @class_name = class_name
107
+ @status = status.to_s if STATUSES.include?(status.to_s)
64
108
  end
65
109
 
66
110
  def call
67
- # safe_joins ensure multiple <td> elements render correctly
68
- content_tag(:tr, safe_join(cells), class:@css_class)
111
+ content_tag(
112
+ :tr,
113
+ safe_join(cells),
114
+ class: class_names(@class_name, @status && "lesli-table__row--#{@status}")
115
+ )
69
116
  end
70
117
 
71
118
  class TableData < ViewComponent::Base
72
- attr_reader :css_class
119
+ attr_reader :class_name
73
120
 
74
- def initialize(css_class: "")
75
- @css_class = css_class
121
+ def initialize(class_name: nil, align: nil)
122
+ @class_name = class_names(
123
+ class_name,
124
+ ("lesli-table__cell--#{align}" if %i[left center right].include?(align&.to_sym))
125
+ )
76
126
  end
77
127
 
78
128
  def call
79
- content_tag :td, content, class: css_class
129
+ content_tag(:td, content, class: class_name)
80
130
  end
81
131
  end
82
132
  end
@@ -35,9 +35,22 @@ Building a better future, one line of code at a time.
35
35
  module LesliView
36
36
  module Forms
37
37
  class Builder < ActionView::Helpers::FormBuilder
38
- def css_category category=nil
39
- return "is-#{category}"
40
- end
38
+ CATEGORY_ALIASES = {
39
+ alert: :danger,
40
+ error: :danger,
41
+ notice: :info
42
+ }.freeze
43
+
44
+ def css_category(category = nil)
45
+ return if category.blank?
46
+
47
+ normalized_category = CATEGORY_ALIASES.fetch(category.to_sym, category.to_sym)
48
+ "is-#{normalized_category}"
49
+ end
50
+
51
+ def merge_css_classes(*classes)
52
+ classes.flatten.compact.flat_map { |value| value.to_s.split }.reject(&:blank?).uniq.join(" ")
53
+ end
41
54
 
42
55
  include LesliView::Forms::Fields
43
56
  include LesliView::Forms::Inputs
@@ -1,25 +1,49 @@
1
1
  module LesliView
2
2
  module Forms
3
3
  class BuilderHorizontal < Builder
4
- def field_control(attribute, label: nil, message:nil, category:nil, icon:nil)
5
- super(attribute, label:label, message:message, category:category, icon:icon, horizontal:true)
4
+ def field_control(attribute, label: nil, message: nil, category: nil, icon: nil, **options)
5
+ super(attribute, label: label, message: message, category: category, icon: icon, horizontal: true, **options)
6
6
  end
7
7
 
8
- def field_select(attribute, choices, options = {}, html_options = {}, label: nil, humanize: true)
9
- super(attribute, choices, options, html_options, label: label, humanize: humanize, horizontal: true)
8
+ def field_control_text(attribute, label: nil, message: nil, category: nil, icon: nil, **options)
9
+ super(attribute, label: label, message: message, category: category, icon: icon, horizontal: true, **options)
10
+ end
11
+
12
+ def field_control_email(attribute, label: nil, message: nil, category: nil, icon: nil, **options)
13
+ super(attribute, label: label, message: message, category: category, icon: icon, horizontal: true, **options)
14
+ end
15
+
16
+ def field_control_checkbox(attribute, label: nil, message: nil, category: nil, icon: nil, **options)
17
+ super(attribute, label: label, message: message, category: category, icon: icon, horizontal: true, **options)
18
+ end
19
+
20
+ def field_control_password(attribute, label: nil, message: nil, category: nil, icon: nil, **options)
21
+ super(attribute, label: label, message: message, category: category, icon: icon, horizontal: true, **options)
22
+ end
23
+
24
+ def field_control_textarea(attribute, label: nil, message: nil, category: nil, icon: nil, **options)
25
+ super(attribute, label: label, message: message, category: category, icon: icon, horizontal: true, **options)
26
+ end
27
+
28
+ def field_textarea(attribute, **options)
29
+ field_control_textarea(attribute, **options)
30
+ end
31
+
32
+ def field_select(attribute, choices, options = {}, html_options = {}, label: nil, humanize: true, **field_options)
33
+ field_control_select(attribute, choices, label: label, humanize: humanize, options: options, html_options: html_options, **field_options)
10
34
  end
11
35
 
12
36
  def field_control_submit(value = nil, options = {})
13
37
  super(value, options, horizontal:true)
14
38
  end
15
39
 
16
- def field_control_button(value = nil, options = {}, icon:nil, horizontal: false)
17
- super(value, options, icon:icon, horizontal:true)
18
- end
40
+ def field_control_button(value = nil, options = {}, category: "primary", icon: nil, type: "submit")
41
+ super(value, options, category: category, icon: icon, type: type, horizontal: true)
42
+ end
19
43
 
20
- def field_control_select(attribute, choices, label: nil, message:nil, category:nil, icon:nil, humanize:true)
21
- super(attribute, choices, label:label, message:message, category:category, icon:icon, horizontal:true, humanize:humanize)
22
- end
44
+ def field_control_select(attribute, choices, label: nil, message: nil, category: nil, icon: nil, humanize: true, options: {}, html_options: {})
45
+ super(attribute, choices, label: label, message: message, category: category, icon: icon, horizontal: true, humanize: humanize, options: options, html_options: html_options)
46
+ end
23
47
  end
24
48
  end
25
49
  end