lesli_view 1.1.5 → 1.1.6

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d2ab5f67c484b445f4d9831fb796410b9d2328a1c4338bc585925f1e76944415
4
- data.tar.gz: 5e611cf24047181f19ffefbe495247afe0171c45c0d258ccb12912fed6b057be
3
+ metadata.gz: d42e452e24ee35eb0a02019b62748ffe8c2cdaa42da6a7047292deda3134a3d3
4
+ data.tar.gz: 8b70b0bfd4aa3f64600a35d4cc4c59bee46561ecb9978dc22b18f7b577c98c8c
5
5
  SHA512:
6
- metadata.gz: 3d830e3e4d0bb5230fec59b4b2b96b6d7465c2cf9cb6e435b4241c13cfcc0c3ec74aa12995b16d477a02123951bd2258fbabe87eb415b0892cde0cd260bb44e5
7
- data.tar.gz: b99497cb2c0cdecddcfcafc81d9a6801b0f30ae673f91449b5cd846b5e4811d55d5cfd4bc87610370c33b12c353e2017f07f626146d5646a3b096aca39eea07a
6
+ metadata.gz: 24785b4a4dfa224ce97181642e986fd4c51601e267c7b9dc3565003a073e6baf3c0fcacab9a87f98067b43eae15d2763a9f007dff48dc61e03a37433d45f9ec0
7
+ data.tar.gz: 94bedf88e3f7a79e63b59734e7c62f46b304d77110dab101494a15463685d47c57bb63ecc41d4915a11fd7605ebf141e5c86616c845ab4c0763228cdc156ffe3
data/Rakefile CHANGED
@@ -1,6 +1,14 @@
1
1
  require "bundler/setup"
2
+ require "rake/testtask"
2
3
 
3
4
  APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
4
5
  load "rails/tasks/engine.rake"
5
6
 
6
7
  require "bundler/gem_tasks"
8
+
9
+ Rake::TestTask.new(:test) do |test|
10
+ test.libs << "test"
11
+ test.pattern = "test/**/*_test.rb"
12
+ end
13
+
14
+ task default: :test
@@ -36,6 +36,9 @@ Building a better future, one line of code at a time.
36
36
  :root, :host {
37
37
  --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', 'Noto Sans', Arial,
38
38
  sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
39
+ --color-red-50: oklch(97.1% 0.013 17.38);
40
+ --color-red-500: oklch(63.7% 0.237 25.331);
41
+ --color-red-600: oklch(57.7% 0.245 27.325);
39
42
  --color-amber-50: oklch(98.7% 0.022 95.277);
40
43
  --color-amber-100: oklch(96.2% 0.059 95.617);
41
44
  --color-amber-200: oklch(92.4% 0.12 95.746);
@@ -123,10 +126,12 @@ Building a better future, one line of code at a time.
123
126
  --text-6xl: 3.75rem;
124
127
  --text-6xl--line-height: 1;
125
128
  --font-weight-light: 300;
129
+ --font-weight-normal: 400;
126
130
  --font-weight-medium: 500;
127
131
  --font-weight-semibold: 600;
128
132
  --font-weight-bold: 700;
129
133
  --tracking-tight: -0.025em;
134
+ --tracking-normal: 0em;
130
135
  --tracking-wide: 0.025em;
131
136
  --radius-sm: 0.25rem;
132
137
  --radius-md: 0.375rem;
@@ -374,9 +379,6 @@ Building a better future, one line of code at a time.
374
379
  --tw-translate-x: calc(calc(1 / 2 * 100%) * -1);
375
380
  translate: var(--tw-translate-x) var(--tw-translate-y);
376
381
  }
377
- .cursor-not-allowed {
378
- cursor: not-allowed;
379
- }
380
382
  .cursor-pointer {
381
383
  cursor: pointer;
382
384
  }
@@ -606,6 +608,9 @@ Building a better future, one line of code at a time.
606
608
  .p-6 {
607
609
  padding: calc(var(--spacing) * 6);
608
610
  }
611
+ .px-3 {
612
+ padding-inline: calc(var(--spacing) * 3);
613
+ }
609
614
  .px-4 {
610
615
  padding-inline: calc(var(--spacing) * 4);
611
616
  }
@@ -615,6 +620,9 @@ Building a better future, one line of code at a time.
615
620
  .py-0\.5 {
616
621
  padding-block: calc(var(--spacing) * 0.5);
617
622
  }
623
+ .py-2 {
624
+ padding-block: calc(var(--spacing) * 2);
625
+ }
618
626
  .py-2\.5 {
619
627
  padding-block: calc(var(--spacing) * 2.5);
620
628
  }
@@ -727,6 +735,10 @@ Building a better future, one line of code at a time.
727
735
  --tw-font-weight: var(--font-weight-medium);
728
736
  font-weight: var(--font-weight-medium);
729
737
  }
738
+ .font-normal {
739
+ --tw-font-weight: var(--font-weight-normal);
740
+ font-weight: var(--font-weight-normal);
741
+ }
730
742
  .font-semibold {
731
743
  --tw-font-weight: var(--font-weight-semibold);
732
744
  font-weight: var(--font-weight-semibold);
@@ -735,6 +747,10 @@ Building a better future, one line of code at a time.
735
747
  --tw-tracking: 0.2em;
736
748
  letter-spacing: 0.2em;
737
749
  }
750
+ .tracking-normal {
751
+ --tw-tracking: var(--tracking-normal);
752
+ letter-spacing: var(--tracking-normal);
753
+ }
738
754
  .tracking-tight {
739
755
  --tw-tracking: var(--tracking-tight);
740
756
  letter-spacing: var(--tracking-tight);
@@ -752,6 +768,9 @@ Building a better future, one line of code at a time.
752
768
  .whitespace-nowrap {
753
769
  white-space: nowrap;
754
770
  }
771
+ .whitespace-pre-wrap {
772
+ white-space: pre-wrap;
773
+ }
755
774
  .text-\[var\(--lesli-color-primary\)\] {
756
775
  color: var(--lesli-color-primary);
757
776
  }
@@ -764,6 +783,9 @@ Building a better future, one line of code at a time.
764
783
  .text-cyan-500 {
765
784
  color: var(--color-cyan-500);
766
785
  }
786
+ .text-red-600 {
787
+ color: var(--color-red-600);
788
+ }
767
789
  .text-sky-600 {
768
790
  color: var(--color-sky-600);
769
791
  }
@@ -788,12 +810,18 @@ Building a better future, one line of code at a time.
788
810
  .text-slate-800 {
789
811
  color: var(--color-slate-800);
790
812
  }
813
+ .text-slate-900 {
814
+ color: var(--color-slate-900);
815
+ }
791
816
  .text-white {
792
817
  color: var(--color-white);
793
818
  }
794
819
  .capitalize {
795
820
  text-transform: capitalize;
796
821
  }
822
+ .normal-case {
823
+ text-transform: none;
824
+ }
797
825
  .uppercase {
798
826
  text-transform: uppercase;
799
827
  }
@@ -876,6 +904,33 @@ Building a better future, one line of code at a time.
876
904
  .group-last\:hidden:is(:where(.group):last-child *) {
877
905
  display: none;
878
906
  }
907
+ .file\:mr-4::file-selector-button {
908
+ margin-right: calc(var(--spacing) * 4);
909
+ }
910
+ .file\:border-0::file-selector-button {
911
+ border-style: var(--tw-border-style);
912
+ border-width: 0px;
913
+ }
914
+ .file\:bg-slate-100::file-selector-button {
915
+ background-color: var(--color-slate-100);
916
+ }
917
+ .file\:px-4::file-selector-button {
918
+ padding-inline: calc(var(--spacing) * 4);
919
+ }
920
+ .file\:py-2\.5::file-selector-button {
921
+ padding-block: calc(var(--spacing) * 2.5);
922
+ }
923
+ .file\:text-sm::file-selector-button {
924
+ font-size: var(--text-sm);
925
+ line-height: var(--tw-leading, var(--text-sm--line-height));
926
+ }
927
+ .file\:font-medium::file-selector-button {
928
+ --tw-font-weight: var(--font-weight-medium);
929
+ font-weight: var(--font-weight-medium);
930
+ }
931
+ .file\:text-slate-700::file-selector-button {
932
+ color: var(--color-slate-700);
933
+ }
879
934
  .last\:border-b-0:last-child {
880
935
  border-bottom-style: var(--tw-border-style);
881
936
  border-bottom-width: 0px;
@@ -887,12 +942,21 @@ Building a better future, one line of code at a time.
887
942
  .hover\:border-slate-300:hover {
888
943
  border-color: var(--color-slate-300);
889
944
  }
945
+ .hover\:border-slate-400:hover {
946
+ border-color: var(--color-slate-400);
947
+ }
948
+ .hover\:bg-red-50:hover {
949
+ background-color: var(--color-red-50);
950
+ }
890
951
  .hover\:bg-sky-50:hover {
891
952
  background-color: var(--color-sky-50);
892
953
  }
893
954
  .hover\:bg-slate-50:hover {
894
955
  background-color: var(--color-slate-50);
895
956
  }
957
+ .hover\:bg-slate-100:hover {
958
+ background-color: var(--color-slate-100);
959
+ }
896
960
  .hover\:text-sky-800:hover {
897
961
  color: var(--color-sky-800);
898
962
  }
@@ -902,11 +966,17 @@ Building a better future, one line of code at a time.
902
966
  .hover\:underline:hover {
903
967
  text-decoration-line: underline;
904
968
  }
969
+ .hover\:file\:bg-slate-200:hover::file-selector-button {
970
+ background-color: var(--color-slate-200);
971
+ }
905
972
  }
906
973
  .focus\:ring-2:focus {
907
974
  --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
908
975
  box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
909
976
  }
977
+ .focus\:ring-\[var\(--lesli-color-primary\)\]:focus {
978
+ --tw-ring-color: var(--lesli-color-primary);
979
+ }
910
980
  .focus\:ring-sky-500\/30:focus {
911
981
  --tw-ring-color: color-mix(in srgb, oklch(68.5% 0.169 237.323) 30%, transparent);
912
982
  @supports (color: color-mix(in lab, red, red)) {
@@ -933,6 +1003,12 @@ Building a better future, one line of code at a time.
933
1003
  .focus-visible\:ring-blue-500:focus-visible {
934
1004
  --tw-ring-color: var(--color-blue-500);
935
1005
  }
1006
+ .focus-visible\:ring-red-500\/40:focus-visible {
1007
+ --tw-ring-color: color-mix(in srgb, oklch(63.7% 0.237 25.331) 40%, transparent);
1008
+ @supports (color: color-mix(in lab, red, red)) {
1009
+ --tw-ring-color: color-mix(in oklab, var(--color-red-500) 40%, transparent);
1010
+ }
1011
+ }
936
1012
  .focus-visible\:outline-2:focus-visible {
937
1013
  outline-style: var(--tw-outline-style);
938
1014
  outline-width: 2px;
@@ -950,9 +1026,6 @@ Building a better future, one line of code at a time.
950
1026
  .focus-visible\:ring-inset:focus-visible {
951
1027
  --tw-ring-inset: inset;
952
1028
  }
953
- .disabled\:cursor-not-allowed:disabled {
954
- cursor: not-allowed;
955
- }
956
1029
  @media (prefers-reduced-motion: no-preference) {
957
1030
  .motion-safe\:animate-pulse {
958
1031
  animation: var(--animate-pulse);
@@ -980,6 +1053,12 @@ Building a better future, one line of code at a time.
980
1053
  .sm\:flex-row {
981
1054
  flex-direction: row;
982
1055
  }
1056
+ .sm\:items-center {
1057
+ align-items: center;
1058
+ }
1059
+ .sm\:justify-between {
1060
+ justify-content: space-between;
1061
+ }
983
1062
  .sm\:gap-4 {
984
1063
  gap: calc(var(--spacing) * 4);
985
1064
  }
@@ -1054,12 +1133,17 @@ Building a better future, one line of code at a time.
1054
1133
  }
1055
1134
  }
1056
1135
  @media (width >= 64rem) {
1136
+ .lg\:grid-cols-\[minmax\(0\,1fr\)_minmax\(12rem\,0\.35fr\)_auto\] {
1137
+ grid-template-columns: minmax(0,1fr) minmax(12rem,0.35fr) auto;
1138
+ }
1139
+ .lg\:items-end {
1140
+ align-items: flex-end;
1141
+ }
1057
1142
  .lg\:px-8 {
1058
1143
  padding-inline: calc(var(--spacing) * 8);
1059
1144
  }
1060
1145
  }
1061
1146
  }
1062
- @charset "UTF-8";
1063
1147
  :root {
1064
1148
  --lesli-color-primary-50: #F2F7FF;
1065
1149
  --lesli-color-primary-100: #E3EEFF;
@@ -1,29 +1,42 @@
1
1
  <div class="lesli-timelines py-6">
2
- <ul class="m-0 w-full list-none p-0">
3
- <% activities.each do |activity| %>
4
- <li class="group grid grid-cols-[1.5rem_minmax(0,1fr)] gap-x-4 md:grid-cols-[minmax(8rem,auto)_1.5rem_minmax(0,1fr)] md:gap-x-6">
5
- <div class="col-start-2 mb-2 min-w-0 text-xs font-bold uppercase tracking-[0.2em] text-slate-500 md:col-start-1 md:row-start-1 md:mb-0 md:pt-1 md:text-right">
6
- <span><%= activity['date'] %></span>
7
- </div>
2
+ <% if activities.any? %>
3
+ <ul class="m-0 w-full list-none p-0">
4
+ <% activities.each do |activity| %>
5
+ <li class="group grid grid-cols-[1.5rem_minmax(0,1fr)] gap-x-4 md:grid-cols-[minmax(8rem,auto)_1.5rem_minmax(0,1fr)] md:gap-x-6">
6
+ <div class="col-start-2 mb-2 min-w-0 text-xs font-bold uppercase tracking-[0.2em] text-slate-500 md:col-start-1 md:row-start-1 md:mb-0 md:pt-1 md:text-right">
7
+ <time><%= activity["date"] %></time>
8
8
 
9
- <div class="relative col-start-1 row-span-2 row-start-1 flex justify-center md:col-start-2">
10
- <span
11
- class="material-symbols-outlined relative z-10 bg-white text-xl leading-none text-[var(--lesli-color-primary)]"
12
- aria-hidden="true">
13
- <%= icons.dig(activity['icon']&.to_sym) || 'radio_button_checked' %>
14
- </span>
15
- <span class="absolute top-7 bottom-0 left-1/2 w-px -translate-x-1/2 bg-slate-300 group-last:hidden" aria-hidden="true"></span>
16
- </div>
9
+ <% if activity["actor"].present? %>
10
+ <span class="mt-1 block font-normal normal-case tracking-normal text-slate-400">
11
+ <%= activity["actor"] %>
12
+ </span>
13
+ <% end %>
14
+ </div>
17
15
 
18
- <div class="col-start-2 row-start-2 min-w-0 pb-10 md:col-start-3 md:row-start-1">
19
- <h6 class="text-base font-semibold text-slate-800">
20
- <%= activity['operation'] %>
21
- </h6>
22
- <p class="mt-1 text-sm leading-6 text-slate-600">
23
- <%= activity['description'] %>
24
- </p>
25
- </div>
26
- </li>
27
- <% end %>
28
- </ul>
16
+ <div class="relative col-start-1 row-span-2 row-start-1 flex justify-center md:col-start-2">
17
+ <span
18
+ class="material-symbols-outlined relative z-10 bg-white text-xl leading-none text-[var(--lesli-color-primary)]"
19
+ aria-hidden="true">
20
+ <%= icon_for(activity) %>
21
+ </span>
22
+ <span class="absolute top-7 bottom-0 left-1/2 w-px -translate-x-1/2 bg-slate-300 group-last:hidden" aria-hidden="true"></span>
23
+ </div>
24
+
25
+ <div class="col-start-2 row-start-2 min-w-0 pb-10 md:col-start-3 md:row-start-1">
26
+ <h6 class="text-base font-semibold text-slate-800">
27
+ <%= activity["operation"] %>
28
+ </h6>
29
+
30
+ <% if activity["description"].present? %>
31
+ <p class="mt-1 text-sm leading-6 text-slate-600">
32
+ <%= activity["description"] %>
33
+ </p>
34
+ <% end %>
35
+ </div>
36
+ </li>
37
+ <% end %>
38
+ </ul>
39
+ <% else %>
40
+ <%= render LesliView::Elements::Empty.new(text: empty_text) %>
41
+ <% end %>
29
42
  </div>
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "lesli_view/elements/empty"
4
+
3
5
  =begin
4
6
 
5
7
  Lesli
@@ -35,12 +37,23 @@ Building a better future, one line of code at a time.
35
37
  module LesliView
36
38
  module Components
37
39
  class Timeline < ViewComponent::Base
38
- attr_reader :activities, :icons
40
+ DEFAULT_ICON = "radio_button_checked"
41
+
42
+ attr_reader :activities, :empty_text, :icons
43
+
44
+ def initialize(
45
+ activities: [],
46
+ icons: {},
47
+ empty_text: "No activities have been recorded yet."
48
+ )
49
+ @activities = Array(activities)
50
+ @icons = icons.transform_keys(&:to_sym)
51
+ @empty_text = empty_text
52
+ end
39
53
 
40
- def initialize(activities: nil, icons:{})
41
- @icons=icons
42
- @activities = activities
54
+ def icon_for(activity)
55
+ icons.fetch(activity["icon"]&.to_sym, DEFAULT_ICON)
43
56
  end
44
- end
57
+ end
45
58
  end
46
59
  end
@@ -25,6 +25,10 @@ module LesliView
25
25
  super(attribute, label: label, message: message, category: category, icon: icon, horizontal: true, **options)
26
26
  end
27
27
 
28
+ def field_control_textarea_rich(attribute, label: nil, message: nil, category: nil, icon: nil, **options)
29
+ super(attribute, label: label, message: message, category: category, icon: icon, horizontal: true, **options)
30
+ end
31
+
28
32
  def field_textarea(attribute, **options)
29
33
  field_control_textarea(attribute, **options)
30
34
  end
@@ -81,6 +81,20 @@ module LesliView
81
81
  end
82
82
 
83
83
  def field_control_textarea(attribute, label: nil, message: nil, category: nil, icon: nil, horizontal: false, **options)
84
+ label_html = label == false ? nil : label(attribute, label)
85
+ control_html = text_area(attribute, options)
86
+
87
+ field_control_builder(
88
+ label_html: label_html,
89
+ control_html: control_html,
90
+ message_text: message,
91
+ category: category,
92
+ horizontal: horizontal,
93
+ icon: icon
94
+ )
95
+ end
96
+
97
+ def field_control_textarea_rich(attribute, label: nil, message: nil, category: nil, icon: nil, horizontal: false, **options)
84
98
  label_html = label == false ? nil : label(attribute, label)
85
99
  control_html = rich_textarea(attribute, options)
86
100
 
@@ -32,5 +32,9 @@ Building a better future, one line of code at a time.
32
32
  <article
33
33
  class="lesli-items-activities bg-white p-4 sm:p-8"
34
34
  id="lesli-items-activities">
35
- <%= render(LesliView::Components::Timeline.new(activities: @activities, icons: @icons)) %>
35
+ <%= render LesliView::Components::Timeline.new(
36
+ activities: activities,
37
+ empty_text: empty_text,
38
+ icons: icons
39
+ ) %>
36
40
  </article>
@@ -32,17 +32,117 @@ Building a better future, one line of code at a time.
32
32
  module LesliView
33
33
  module Items
34
34
  class Activities < ViewComponent::Base
35
- attr_reader :activities, :icons
36
-
37
- def initialize(resource, icons:{})
38
- @icons=icons
39
- @activities = resource.activities
40
- .select(
41
- :id,
42
- "description as operation",
43
- "activity_code as icon",
44
- Date2.new.db_column('created_at', as:'date')
45
- ).order(id: :desc).as_json
35
+ DEFAULT_LIMIT = 25
36
+ I18N_SCOPE = "lesli.items.activities"
37
+ DATE_FORMAT = "%Y-%m-%d · %H:%M"
38
+
39
+ attr_reader :activities, :empty_text, :icons, :i18n_scope
40
+
41
+ def initialize(
42
+ resource,
43
+ icons: {},
44
+ i18n_scope: I18N_SCOPE,
45
+ limit: DEFAULT_LIMIT,
46
+ empty_text: nil
47
+ )
48
+ @icons = icons
49
+ @i18n_scope = i18n_scope
50
+ @activities = activity_scope(resource, limit).map do |activity|
51
+ activity_to_timeline(activity)
52
+ end
53
+ @empty_text = empty_text || I18n.t(
54
+ "#{i18n_scope}.empty",
55
+ default: "No activities have been recorded yet."
56
+ )
57
+ end
58
+
59
+ private
60
+
61
+ def activity_to_timeline(activity)
62
+ metadata = activity.metadata.is_a?(Hash) ? activity.metadata : {}
63
+
64
+ {
65
+ "actor" => actor_name(activity.actor),
66
+ "date" => I18n.l(activity.created_at, format: DATE_FORMAT),
67
+ "description" => activity_description(activity, metadata),
68
+ "icon" => activity.activity_code.to_s,
69
+ "operation" => activity_operation(activity)
70
+ }
71
+ end
72
+
73
+ def activity_description(activity, metadata)
74
+ I18n.t(
75
+ "#{activity_i18n_key(activity)}.#{description_key(metadata)}",
76
+ from: snapshot_name(metadata_value(metadata, :from)),
77
+ to: snapshot_name(metadata_value(metadata, :to)),
78
+ default: activity.description.to_s
79
+ ).presence
80
+ end
81
+
82
+ def activity_i18n_key(activity)
83
+ "#{i18n_scope}.events.#{activity.activity_code}"
84
+ end
85
+
86
+ def activity_operation(activity)
87
+ I18n.t(
88
+ "#{activity_i18n_key(activity)}.operation",
89
+ default: activity.activity_code.to_s.humanize
90
+ )
91
+ end
92
+
93
+ def activity_scope(resource, limit)
94
+ resource.activities
95
+ .includes(:actor)
96
+ .reorder(created_at: :desc, id: :desc)
97
+ .limit(normalize_limit(limit))
98
+ end
99
+
100
+ def actor_name(actor)
101
+ full_name = actor.full_name if actor.respond_to?(:full_name)
102
+ name = actor.name if actor.respond_to?(:name)
103
+
104
+ full_name.presence || name.presence || I18n.t(
105
+ "#{i18n_scope}.unknown_actor",
106
+ default: "Unknown user"
107
+ )
108
+ end
109
+
110
+ def description_key(metadata)
111
+ has_change = metadata.key?(:from) || metadata.key?("from") ||
112
+ metadata.key?(:to) || metadata.key?("to")
113
+
114
+ return "description" unless has_change
115
+ return "set" if metadata_value(metadata, :from).nil?
116
+ return "removed" if metadata_value(metadata, :to).nil?
117
+
118
+ "changed"
119
+ end
120
+
121
+ def metadata_value(metadata, key)
122
+ return metadata[key] if metadata.key?(key)
123
+
124
+ metadata[key.to_s]
125
+ end
126
+
127
+ def normalize_limit(limit)
128
+ normalized_limit = Integer(limit)
129
+ return normalized_limit if normalized_limit.positive?
130
+
131
+ raise ArgumentError, "limit must be a positive integer"
132
+ rescue TypeError, ArgumentError
133
+ raise ArgumentError, "limit must be a positive integer"
134
+ end
135
+
136
+ def snapshot_name(snapshot)
137
+ return "" if snapshot.nil?
138
+ return snapshot.to_s unless snapshot.is_a?(Hash)
139
+
140
+ [:name, :subject, :uid, :id].each do |key|
141
+ value = metadata_value(snapshot, key)
142
+ return value.to_s if value.present?
143
+ end
144
+
145
+ ""
46
146
  end
47
147
  end
48
148
  end
@@ -0,0 +1,45 @@
1
+ <li
2
+ id="<%= dom_id(resource) %>"
3
+ class="flex flex-col gap-4 rounded-xl border border-slate-200 bg-white p-4 sm:flex-row sm:items-center sm:justify-between">
4
+ <div class="flex min-w-0 items-center gap-3">
5
+ <span
6
+ class="material-symbols-outlined shrink-0 text-3xl text-[var(--lesli-color-primary)]"
7
+ aria-hidden="true">
8
+ attach_file
9
+ </span>
10
+
11
+ <div class="min-w-0">
12
+ <p class="truncate text-sm font-medium text-slate-900" title="<%= filename %>">
13
+ <%= display_name %>
14
+ </p>
15
+ <p class="mt-1 text-xs text-slate-500">
16
+ <%= details %>
17
+ </p>
18
+ </div>
19
+ </div>
20
+
21
+ <div class="flex shrink-0 items-center gap-2">
22
+ <%= link_to(
23
+ I18n.t("lesli.items.attachments.download", default: "Download"),
24
+ path,
25
+ class: "inline-flex items-center gap-2 rounded-lg border border-slate-300 px-3 py-2 text-sm font-medium text-slate-700 transition-colors hover:border-slate-400 hover:bg-slate-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--lesli-color-primary)]",
26
+ data: { turbo: false }
27
+ ) %>
28
+
29
+ <%= button_to(
30
+ I18n.t("lesli.items.attachments.remove", default: "Remove"),
31
+ path,
32
+ method: :delete,
33
+ class: "inline-flex items-center rounded-lg px-3 py-2 text-sm font-medium text-red-600 transition-colors hover:bg-red-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-500/40",
34
+ form: {
35
+ class: "inline-flex",
36
+ data: {
37
+ turbo_confirm: I18n.t(
38
+ "lesli.items.attachments.confirm_remove",
39
+ default: "Remove this file?"
40
+ )
41
+ }
42
+ }
43
+ ) %>
44
+ </div>
45
+ </li>
@@ -0,0 +1,63 @@
1
+ =begin
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
+ =end
32
+
33
+ module LesliView
34
+ module Items
35
+ class Attachment < ViewComponent::Base
36
+ attr_reader :resource
37
+
38
+ def initialize(attachment, path: nil)
39
+ @resource = attachment
40
+ @path = path
41
+ end
42
+
43
+ def path
44
+ @path || helpers.polymorphic_path(resource)
45
+ end
46
+
47
+ def filename
48
+ resource.file.filename.to_s
49
+ end
50
+
51
+ def display_name
52
+ resource.respond_to?(:display_name) ? resource.display_name : filename
53
+ end
54
+
55
+ def details
56
+ [
57
+ resource.file.blob.content_type,
58
+ helpers.number_to_human_size(resource.file.blob.byte_size)
59
+ ].compact.join(" · ")
60
+ end
61
+ end
62
+ end
63
+ end
@@ -29,40 +29,75 @@ Building a better future, one line of code at a time.
29
29
  // · ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~
30
30
  // ·
31
31
  %>
32
- <style>
33
- .lesli-items-attachments {
34
- .files {
35
- display:flex;
36
- .file-list {
37
- padding: .2rem 1rem;
38
- text-align: center;
39
- span {
40
- font-size: 2.8rem;
41
- }
42
- }
43
- }
44
- }
45
- </style>
46
- <article class="lesli-items-attachments box p-6" id="lesli-items-attachments">
47
- <div class="file is-boxed">
48
- <label class="file-label">
49
- <input class="file-input" type="file" name="resume" />
50
- <span class="file-cta">
51
- <span class="material-symbols-outlined">
52
- upload_file
53
- </span>
54
- <span class="file-label"> Choose a file… </span>
55
- </span>
56
- </label>
57
- </div>
58
-
59
- <div class="files">
60
- <% ['file-word-2-line','file-excel-2-line','file-pdf-2-line'].each do |file| %>
61
- <div class="file-list">
62
- <span class="ri-<%= file %>"></span>
63
- <p><%= file %></p>
32
+ <article
33
+ class="lesli-items-attachments rounded-2xl border border-slate-200 bg-white p-6 shadow-sm"
34
+ id="<%= dom_id(resource, :attachments) %>">
35
+ <%= form_with(
36
+ scope: scope_key,
37
+ model: attachment,
38
+ builder: LesliView::Forms::Builder,
39
+ url: create_path,
40
+ multipart: true
41
+ ) do |form| %>
42
+ <div class="grid gap-4 lg:grid-cols-[minmax(0,1fr)_minmax(12rem,0.35fr)_auto] lg:items-end">
43
+ <div>
44
+ <%= form.label(
45
+ :file,
46
+ I18n.t("lesli.items.attachments.choose_file", default: "Choose a file"),
47
+ class: "mb-2 block text-sm font-medium text-slate-700"
48
+ ) %>
49
+ <%= form.file_field(
50
+ :file,
51
+ required: true,
52
+ accept: accepted_content_types,
53
+ class: "block w-full cursor-pointer rounded-lg border border-slate-300 bg-white text-sm text-slate-700 file:mr-4 file:border-0 file:bg-slate-100 file:px-4 file:py-2.5 file:text-sm file:font-medium file:text-slate-700 hover:file:bg-slate-200 focus:outline-none focus:ring-2 focus:ring-[var(--lesli-color-primary)]"
54
+ ) %>
55
+ <% if attachment.errors[:file].any? %>
56
+ <p class="mt-2 text-sm text-red-600">
57
+ <%= attachment.errors[:file].to_sentence %>
58
+ </p>
59
+ <% end %>
64
60
  </div>
65
- <% end %>
66
- </div>
61
+
62
+ <%= form.field_control_text(
63
+ :label,
64
+ label: I18n.t("lesli.items.attachments.label", default: "Label"),
65
+ maxlength: 255
66
+ ) %>
67
+
68
+ <%= form.field_control_button(
69
+ I18n.t("lesli.items.attachments.add", default: "Add file"),
70
+ {
71
+ data: {
72
+ turbo_submits_with: I18n.t(
73
+ "lesli.items.attachments.uploading",
74
+ default: "Uploading..."
75
+ )
76
+ }
77
+ },
78
+ icon: "upload_file"
79
+ ) %>
80
+ </div>
81
+ <% end %>
82
+
83
+ <hr class="my-6 border-0 border-t border-slate-200">
84
+
85
+ <% if attachments.any? %>
86
+ <ul class="m-0 list-none space-y-3 p-0">
87
+ <% attachments.each do |item| %>
88
+ <%= render LesliView::Items::Attachment.new(
89
+ item,
90
+ path: attachment_path(item)
91
+ ) %>
92
+ <% end %>
93
+ </ul>
94
+ <% else %>
95
+ <p class="py-6 text-center text-sm text-slate-500">
96
+ <%= I18n.t(
97
+ "lesli.items.attachments.empty",
98
+ default: "No files have been attached yet."
99
+ ) %>
100
+ </p>
101
+ <% end %>
67
102
  </article>
68
103
 
@@ -33,12 +33,48 @@ Building a better future, one line of code at a time.
33
33
  module LesliView
34
34
  module Items
35
35
  class Attachments < ViewComponent::Base
36
- attr_reader :resource, :path_to_create
36
+ attr_reader :resource, :attachment
37
37
 
38
- def initialize(resource, path_to_create:nil, path_to_update:nil)
38
+ def initialize(
39
+ resource,
40
+ attachment: nil,
41
+ path_to_create: nil,
42
+ path_to_update: nil
43
+ )
39
44
  @resource = resource
40
45
  @path_to_create = path_to_create
41
46
  @path_to_update = path_to_update
47
+ @attachment = attachment || attachment_class.new
48
+ end
49
+
50
+ def attachment_class
51
+ resource.class.lesli_attachments_class
52
+ end
53
+
54
+ def attachments
55
+ resource.attachments.with_attached_file
56
+ end
57
+
58
+ def create_path
59
+ @path_to_create || helpers.polymorphic_path(
60
+ [:items, :attachments],
61
+ attachable_type: resource.class.name,
62
+ attachable_id: resource.id
63
+ )
64
+ end
65
+
66
+ def attachment_path(item)
67
+ return @path_to_update.call(item) if @path_to_update.respond_to?(:call)
68
+
69
+ helpers.polymorphic_path(item)
70
+ end
71
+
72
+ def scope_key
73
+ ActiveSupport::Inflector.underscore(attachment_class.name).tr("/", "_")
74
+ end
75
+
76
+ def accepted_content_types
77
+ attachment_class.lesli_attachment_content_types.join(",")
42
78
  end
43
79
  end
44
80
  end
@@ -1,10 +1,12 @@
1
1
  <li class="lesli-items-discussions-comments-comment flex flex-col gap-3 border-b border-slate-200 pb-6 last:border-b-0 last:pb-0 sm:flex-row sm:gap-4">
2
2
  <div class="shrink-0 text-sm leading-5 text-slate-500 sm:w-40 sm:text-right">
3
- <strong class="block font-semibold text-slate-800">Luis Donis</strong>
4
- <%= discussion['created_at_string'] || Date2.new.date_time %>
3
+ <strong class="block font-semibold text-slate-800">
4
+ <%= author_name %>
5
+ </strong>
6
+ <time datetime="<%= created_at_iso8601 %>">
7
+ <%= created_at_label %>
8
+ </time>
5
9
  </div>
6
10
 
7
- <div class="lexxy-content min-w-0 flex-1 break-words text-slate-700">
8
- <%== discussion.message %>
9
- </div>
11
+ <div class="min-w-0 flex-1 whitespace-pre-wrap break-words text-slate-700"><%= discussion.message %></div>
10
12
  </li>
@@ -37,6 +37,32 @@ module LesliView
37
37
  def initialize(discussion)
38
38
  @discussion = discussion
39
39
  end
40
+
41
+ def author_name
42
+ author = discussion.author
43
+ full_name = author.full_name if author.respond_to?(:full_name)
44
+ name = author.name if author.respond_to?(:name)
45
+
46
+ full_name.presence || name.presence || I18n.t(
47
+ "lesli.items.discussions.unknown_author",
48
+ default: "Unknown user"
49
+ )
50
+ end
51
+
52
+ def created_at_label
53
+ return if discussion.created_at.blank?
54
+
55
+ I18n.l(
56
+ discussion.created_at,
57
+ format: "%Y-%m-%d · %H:%M"
58
+ )
59
+ end
60
+
61
+ def created_at_iso8601
62
+ return if discussion.created_at.blank?
63
+
64
+ discussion.created_at.strftime("%Y-%m-%dT%H:%M:%S%:z")
65
+ end
40
66
  end
41
67
  end
42
68
  end
@@ -29,31 +29,46 @@ Building a better future, one line of code at a time.
29
29
  // · ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~
30
30
  // ·
31
31
  %>
32
- <article class="lesli-items-discussions bg-white p-4 sm:p-8" id="lesli-items-discussions">
32
+ <article
33
+ class="lesli-items-discussions bg-white p-4 sm:p-8"
34
+ id="<%= dom_id(resource, :discussions) %>">
33
35
  <div class="lesli-items-discussions-form p-4">
34
- <% discussion_class = "#{@resource.class.name.deconstantize}::Items::Discussion".constantize %>
35
- <% scope_key = ActiveSupport::Inflector.underscore(discussion_class.name).tr("/", "_") %>
36
36
  <%= form_with(
37
37
  scope: scope_key,
38
- model: discussion_class.new,
38
+ model: comment,
39
39
  builder: LesliView::Forms::Builder,
40
- url: helpers.polymorphic_path([:items, :discussions],
41
- discussable_type:@resource.class.name,
42
- discussable_id: @resource.id
43
- )) do |form| %>
44
- <%= form.field_control_textarea(:message) %>
45
- <%= form.field_control_submit("Add comment") %>
40
+ url: create_path
41
+ ) do |form| %>
42
+ <%= form.field_control_textarea(:message, rows: 4) %>
43
+ <% if comment.errors[:message].any? %>
44
+ <p class="mt-2 text-sm text-red-600">
45
+ <%= comment.errors.full_messages_for(:message).to_sentence %>
46
+ </p>
47
+ <% end %>
48
+ <%= form.field_control_submit I18n.t(
49
+ "lesli.items.discussions.add",
50
+ default: "Add comment"
51
+ ) %>
46
52
  <% end %>
47
53
  </div>
48
54
  <hr class="my-6 border-0 border-t border-slate-200">
49
55
 
50
56
  <div class="lesli-items-discussions-container p-4">
51
- <ul
52
- id="<%= "#{scope_key}-lesli-items-discussions-list" %>"
53
- class="m-0 list-none space-y-6 p-0">
54
- <% @resource.discussions.order(created_at: :desc).each do |comment| %>
55
- <%= render(LesliView::Items::Discussion.new(comment)) %>
56
- <% end %>
57
- </ul>
57
+ <% if discussions.any? %>
58
+ <ul
59
+ id="<%= "#{scope_key}-lesli-items-discussions-list" %>"
60
+ class="m-0 list-none space-y-6 p-0">
61
+ <% discussions.each do |item| %>
62
+ <%= render(LesliView::Items::Discussion.new(item)) %>
63
+ <% end %>
64
+ </ul>
65
+ <% else %>
66
+ <p class="py-6 text-center text-sm text-slate-500">
67
+ <%= I18n.t(
68
+ "lesli.items.discussions.empty",
69
+ default: "No comments have been added yet."
70
+ ) %>
71
+ </p>
72
+ <% end %>
58
73
  </div>
59
74
  </article>
@@ -32,13 +32,35 @@ Building a better future, one line of code at a time.
32
32
  module LesliView
33
33
  module Items
34
34
  class Discussions < ViewComponent::Base
35
- attr_reader :resource, :path
36
-
37
- delegate :lexxy_rich_textarea_tag, to: :helpers
35
+ attr_reader :comment, :resource
38
36
 
39
- def initialize(resource, path=nil)
37
+ def initialize(resource, comment: nil)
40
38
  @resource = resource
41
- @path = path
39
+ @comment = comment || discussion_class.new
40
+ end
41
+
42
+ def discussion_class
43
+ resource.class.lesli_discussions_class
44
+ end
45
+
46
+ def discussions
47
+ resource.public_send(
48
+ resource.class.lesli_discussions_association_name
49
+ )
50
+ end
51
+
52
+ def create_path
53
+ helpers.polymorphic_path(
54
+ [:items, :discussions],
55
+ discussable_type: resource.class.name,
56
+ discussable_id: resource.id
57
+ )
58
+ end
59
+
60
+ def scope_key
61
+ ActiveSupport::Inflector
62
+ .underscore(discussion_class.name)
63
+ .tr("/", "_")
42
64
  end
43
65
  end
44
66
  end
@@ -32,7 +32,7 @@ Building a better future, one line of code at a time.
32
32
 
33
33
  <li
34
34
  id="<%= dom_id(@resource, @scope_key) %>"
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" %>">
35
+ class="lesli-items-tasks-item rounded-xl border transition-colors <%= @resource.done ? "lesli-items-tasks-task-done border-slate-200 bg-slate-50 hover:border-slate-300 hover:bg-slate-100" : "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,
@@ -42,8 +42,11 @@ Building a better future, one line of code at a time.
42
42
  <%= form.hidden_field :id, value: @resource.id %>
43
43
  <%= form.button(
44
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"
45
+ aria: {
46
+ label: toggle_label,
47
+ pressed: @resource.done?
48
+ },
49
+ 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)]"
47
50
  ) do %>
48
51
  <span class="flex min-w-0 items-center gap-3">
49
52
  <span
@@ -51,9 +54,15 @@ Building a better future, one line of code at a time.
51
54
  aria-hidden="true">
52
55
  <%= @resource.done ? "check_box" : "check_box_outline_blank" %>
53
56
  </span>
54
- <span class="min-w-0 break-words <%= "text-slate-500 line-through" if @resource.done %>">
55
- <%= @resource.title %>
56
- </span>
57
+ <% if @resource.done? %>
58
+ <del class="min-w-0 break-words text-slate-500 line-through">
59
+ <%= @resource.title %>
60
+ </del>
61
+ <% else %>
62
+ <span class="min-w-0 break-words">
63
+ <%= @resource.title %>
64
+ </span>
65
+ <% end %>
57
66
  </span>
58
67
  <% end %>
59
68
  <% end %>
@@ -39,6 +39,20 @@ module LesliView
39
39
  @resource = task
40
40
  @scope_key = scope
41
41
  end
42
+
43
+ def toggle_label
44
+ if resource.done?
45
+ I18n.t(
46
+ "lesli.items.tasks.reopen_action",
47
+ default: "Mark task as incomplete"
48
+ )
49
+ else
50
+ I18n.t(
51
+ "lesli.items.tasks.complete_action",
52
+ default: "Mark task as complete"
53
+ )
54
+ end
55
+ end
42
56
  end
43
57
  end
44
58
  end
@@ -33,29 +33,42 @@ Building a better future, one line of code at a time.
33
33
  class="lesli-items-tasks rounded-2xl border border-slate-200 bg-white p-6 shadow-sm"
34
34
  id="<%= dom_id(@resource, :tasks) %>">
35
35
  <div class="lesli-items-tasks-form">
36
- <% task_class = "#{@resource.class.name.deconstantize}::Items::Task".constantize %>
37
- <% scope_key = ActiveSupport::Inflector.underscore(task_class.name).tr("/", "_") %>
38
36
  <%= form_with(
39
37
  scope: scope_key,
40
- model: task_class.new,
38
+ model: task,
41
39
  builder: LesliView::Forms::Builder,
42
- url: helpers.polymorphic_path([:items, :tasks],
43
- taskable_type:@resource.class.name,
44
- taskable_id: @resource.id
45
- )) do |form| %>
40
+ url: create_path
41
+ ) do |form| %>
46
42
  <%= form.field_control_text :title %>
47
- <%= form.field_control_submit "Add task" %>
43
+ <% if task.errors[:title].any? %>
44
+ <p class="mt-2 text-sm text-red-600">
45
+ <%= task.errors.full_messages_for(:title).to_sentence %>
46
+ </p>
47
+ <% end %>
48
+ <%= form.field_control_submit I18n.t(
49
+ "lesli.items.tasks.add",
50
+ default: "Add task"
51
+ ) %>
48
52
  <% end %>
49
53
  </div>
50
54
  <hr class="my-6 border-0 border-t border-slate-200">
51
55
 
52
56
  <div class="lesli-items-tasks-container">
53
- <ul
54
- id="<%= "#{scope_key}-lesli-items-tasks-list" %>"
55
- class="m-0 list-none space-y-3 p-0">
56
- <% @resource.tasks.each do |task| %>
57
- <%= render(LesliView::Items::Task.new(task, scope_key)) %>
58
- <% end %>
59
- </ul>
57
+ <% if tasks.any? %>
58
+ <ul
59
+ id="<%= "#{scope_key}-lesli-items-tasks-list" %>"
60
+ class="m-0 list-none space-y-3 p-0">
61
+ <% tasks.each do |item| %>
62
+ <%= render(LesliView::Items::Task.new(item, scope_key)) %>
63
+ <% end %>
64
+ </ul>
65
+ <% else %>
66
+ <p class="py-6 text-center text-sm text-slate-500">
67
+ <%= I18n.t(
68
+ "lesli.items.tasks.empty",
69
+ default: "No tasks have been added yet."
70
+ ) %>
71
+ </p>
72
+ <% end %>
60
73
  </div>
61
74
  </article>
@@ -33,10 +33,35 @@ Building a better future, one line of code at a time.
33
33
  module LesliView
34
34
  module Items
35
35
  class Tasks < ViewComponent::Base
36
- attr_reader :resource, :path_to_create
36
+ attr_reader :resource, :task
37
37
 
38
- def initialize(resource)
38
+ def initialize(resource, task: nil)
39
39
  @resource = resource
40
+ @task = task || task_class.new
41
+ end
42
+
43
+ def task_class
44
+ resource.class.lesli_tasks_class
45
+ end
46
+
47
+ def tasks
48
+ resource.public_send(
49
+ resource.class.lesli_tasks_association_name
50
+ )
51
+ end
52
+
53
+ def create_path
54
+ helpers.polymorphic_path(
55
+ [:items, :tasks],
56
+ taskable_type: resource.class.name,
57
+ taskable_id: resource.id
58
+ )
59
+ end
60
+
61
+ def scope_key
62
+ ActiveSupport::Inflector
63
+ .underscore(task_class.name)
64
+ .tr("/", "_")
40
65
  end
41
66
  end
42
67
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LesliView
4
- VERSION = "1.1.5"
5
- BUILD = ""
4
+ VERSION = "1.1.6"
5
+ BUILD = "1792716699"
6
6
  end
data/lib/lesli_view.rb CHANGED
@@ -44,6 +44,7 @@ require "lesli_view/items/actions"
44
44
  require "lesli_view/items/activities"
45
45
  require "lesli_view/items/discussion"
46
46
  require "lesli_view/items/discussions"
47
+ require "lesli_view/items/attachment"
47
48
  require "lesli_view/items/attachments"
48
49
 
49
50
 
data/readme.md CHANGED
@@ -8,10 +8,10 @@
8
8
  <br />
9
9
 
10
10
  <div align="center">
11
- <a target="_blank" href="https://github.com/LesliTech/LesliView/actions/workflows/main.yml">
11
+ <a target="_blank" href="https://github.com/LesliTech/LesliView/actions/workflows/lesli-ci-gem.yaml">
12
12
  <img
13
13
  alt="LesliView test status"
14
- src="https://img.shields.io/github/actions/workflow/status/LesliTech/LesliView/main.yml?branch=main&style=for-the-badge&logo=github&label=tests">
14
+ src="https://img.shields.io/github/actions/workflow/status/LesliTech/LesliView/lesli-ci-gem.yaml?branch=main&style=for-the-badge&logo=github&label=tests">
15
15
  </a>
16
16
  <a target="_blank" href="https://rubygems.org/gems/lesli_view">
17
17
  <img alt="Gem Version" src="https://img.shields.io/gem/v/lesli_view?style=for-the-badge&logo=ruby">
@@ -53,7 +53,7 @@ LesliView provides:
53
53
 
54
54
  The current development baseline is:
55
55
 
56
- - Ruby 3.2 or newer
56
+ - Ruby 3.3 or newer (the release workflow uses Ruby 3.3.5)
57
57
  - Rails 8.1
58
58
  - LesliAssets, configured in the host application's asset build
59
59
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lesli_view
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.5
4
+ version: 1.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - The Lesli Development Team
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-10-20 00:00:00.000000000 Z
11
+ date: 2026-10-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: view_component
@@ -48,7 +48,7 @@ extensions: []
48
48
  extra_rdoc_files: []
49
49
  files:
50
50
  - Rakefile
51
- - app/assets/stylesheets/lesli_view/view.tailwind.css
51
+ - app/assets/stylesheets/lesli_view/view.css
52
52
  - lib/lesli_view.rb
53
53
  - lib/lesli_view/charts/bar.rb
54
54
  - lib/lesli_view/charts/general.html.erb
@@ -89,6 +89,8 @@ files:
89
89
  - lib/lesli_view/items/actions.rb
90
90
  - lib/lesli_view/items/activities.html.erb
91
91
  - lib/lesli_view/items/activities.rb
92
+ - lib/lesli_view/items/attachment.html.erb
93
+ - lib/lesli_view/items/attachment.rb
92
94
  - lib/lesli_view/items/attachments.html.erb
93
95
  - lib/lesli_view/items/attachments.rb
94
96
  - lib/lesli_view/items/discussion.html.erb
@@ -118,7 +120,7 @@ files:
118
120
  - readme.md
119
121
  homepage: https://www.lesli.dev/
120
122
  licenses:
121
- - GPL-3.0
123
+ - GPL-3.0-or-later
122
124
  metadata:
123
125
  homepage_uri: https://www.lesli.dev/
124
126
  changelog_uri: https://github.com/LesliTech/LesliView/releases
@@ -133,7 +135,10 @@ required_ruby_version: !ruby/object:Gem::Requirement
133
135
  requirements:
134
136
  - - ">="
135
137
  - !ruby/object:Gem::Version
136
- version: '0'
138
+ version: '3.3'
139
+ - - "<"
140
+ - !ruby/object:Gem::Version
141
+ version: '4.0'
137
142
  required_rubygems_version: !ruby/object:Gem::Requirement
138
143
  requirements:
139
144
  - - ">="