f_components 0.2.1

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 (43) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +144 -0
  3. data/Rakefile +20 -0
  4. data/app/components/f_components/avatar/component.html.erb +1 -0
  5. data/app/components/f_components/avatar/component.rb +30 -0
  6. data/app/components/f_components/avatar/component.scss +3 -0
  7. data/app/components/f_components/base.rb +14 -0
  8. data/app/components/f_components/button/component.html.erb +5 -0
  9. data/app/components/f_components/button/component.rb +68 -0
  10. data/app/components/f_components/button/component.scss +9 -0
  11. data/app/components/f_components/collapsible/component.html.erb +6 -0
  12. data/app/components/f_components/collapsible/component.rb +15 -0
  13. data/app/components/f_components/collapsible/component.scss +19 -0
  14. data/app/components/f_components/dropdown/component.html.erb +10 -0
  15. data/app/components/f_components/dropdown/component.rb +19 -0
  16. data/app/components/f_components/dropdown/component.scss +10 -0
  17. data/app/components/f_components/form_field/component.html.erb +25 -0
  18. data/app/components/f_components/form_field/component.rb +124 -0
  19. data/app/components/f_components/resource_table/component.html.erb +17 -0
  20. data/app/components/f_components/resource_table/component.rb +139 -0
  21. data/app/components/f_components/table/component.html.erb +23 -0
  22. data/app/components/f_components/table/component.rb +88 -0
  23. data/app/components/f_components/table/component_controller.js +84 -0
  24. data/app/components/f_components/table/desktop/component.html.erb +25 -0
  25. data/app/components/f_components/table/desktop/component.rb +71 -0
  26. data/app/components/f_components/table/mobile/component.html.erb +8 -0
  27. data/app/components/f_components/table/mobile/component.rb +102 -0
  28. data/app/frontend/f_components/stylesheets/blocks/_button.scss +189 -0
  29. data/app/frontend/f_components/stylesheets/f_components.scss +12 -0
  30. data/app/frontend/f_components/stylesheets/variables/_breakpoints.scss +3 -0
  31. data/app/frontend/f_components/stylesheets/variables/_colors.scss +19 -0
  32. data/app/helpers/f_components/application_helper.rb +13 -0
  33. data/app/helpers/f_components/components_helper.rb +33 -0
  34. data/app/helpers/f_components/icons_helper.rb +34 -0
  35. data/config/webpack/development.js +5 -0
  36. data/config/webpack/environment.js +3 -0
  37. data/config/webpack/production.js +7 -0
  38. data/config/webpacker.yml +89 -0
  39. data/lib/f_components/engine.rb +28 -0
  40. data/lib/f_components/railtie.rb +12 -0
  41. data/lib/f_components/version.rb +5 -0
  42. data/lib/f_components.rb +18 -0
  43. metadata +158 -0
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FComponents
4
+ module ResourceTable
5
+ class Component < Base
6
+ attributes :attributes, :resources, resource_class: nil, resource_path: nil,
7
+ class: nil, selectable: {}, actions: true
8
+
9
+ private
10
+
11
+ def table_header
12
+ tag.thead do
13
+ tag.tr class: 'text-left border-b-2 border-gray-lt' do
14
+ row = table_headers
15
+ row.prepend(tag.th(class: 'py-3 px-7 w-2') { nil }) if selectable.present?
16
+ row.append(tag.th(class: 'py-4 px-7')) if actions
17
+
18
+ safe_join(row)
19
+ end
20
+ end
21
+ end
22
+
23
+ def table_headers
24
+ attributes.map do |attribute|
25
+ tag.th(class: 'py-4 px-7') { attribute_name(attribute) }
26
+ end
27
+ end
28
+
29
+ def table_row(resource, class:)
30
+ tag.tr class: binding.local_variable_get(:class) do
31
+ row = []
32
+ row.prepend(tag.td(class: 'py-3 px-7') { form_check_box(resource) }) if selectable.present?
33
+ row.append(table_rows(resource))
34
+ row.append(link_to_resource(resource)) if actions
35
+
36
+ safe_join(row)
37
+ end
38
+ end
39
+
40
+ def table_rows(resource)
41
+ attributes.map { |attribute| tag.td(class: 'py-3 px-7') { row_value(resource, attribute) } }
42
+ end
43
+
44
+ def mobile_header(resource, class:, &block)
45
+ collapsible_title = row_value(resource, attributes.first, class: 'font-bold text-sm')
46
+
47
+ if selectable.present?
48
+ check_box = form_check_box(resource)
49
+ collapsible_title = tag.div(class: 'w-full flex space-x-4 items-center') { check_box + collapsible_title }
50
+ end
51
+
52
+ fcomponent(:collapsible, summary: collapsible_title, class: binding.local_variable_get(:class), &block)
53
+ end
54
+
55
+ def mobile_rows(resource, class:)
56
+ tag.div class: binding.local_variable_get(:class) do
57
+ safe_join(other_attributes.map { |attribute| row(resource, attribute) })
58
+ end
59
+ end
60
+
61
+ def link_to_resource(resource, display_as: :link)
62
+ path = resource_path.present? ? resource_path.(resource) : resource
63
+ return tag.td(class: 'py-3 px-7') { link_to('Detalhes', path) } if display_as == :link
64
+
65
+ fcomponent :button, 'Detalhes', path, type: :secondary, mods: [:sm], class: 'mt-7 mx-auto'
66
+ end
67
+
68
+ def other_attributes
69
+ Array(attributes[1..])
70
+ end
71
+
72
+ def row(resource, attribute)
73
+ tag.div(class: 'flex items-center text-sm') do
74
+ row_label(attribute) + row_value(resource, attribute)
75
+ end
76
+ end
77
+
78
+ def row_label(attribute)
79
+ tag.strong(class: 'w-20 min-w-20 mr-8') { attribute_name(attribute) }
80
+ end
81
+
82
+ def row_value(resource, attribute, class: nil)
83
+ if (attr_value = attribute_value(resource, attribute))
84
+ tag.p(class: "max-w-5/6 #{binding.local_variable_get(:class)}") do
85
+ format_attribute_value(attribute, attr_value)
86
+ end
87
+ else
88
+ attribute_fallback
89
+ end
90
+ end
91
+
92
+ def format_attribute_value(attribute, value)
93
+ return value.to_s unless resource_class.defined_enums.has_key?(attribute.to_s)
94
+
95
+ resource_class.human_enum_name(attribute.to_s, value)
96
+ end
97
+
98
+ def attribute_name(attribute)
99
+ resource_class.human_attribute_name(attribute)
100
+ end
101
+
102
+ def attribute_value(resource, attribute)
103
+ resource.public_send(attribute)
104
+ end
105
+
106
+ def attribute_fallback
107
+ @attribute_fallback ||= tag.p(class: 'text-red') { 'Dados insuficientes' }
108
+ end
109
+
110
+ def resource_class
111
+ @resource_class ||=
112
+ case resources.class.to_s
113
+ when 'ActiveRecord::Relation'
114
+ resources.model
115
+ when 'Draper::CollectionDecorator'
116
+ resources.object.model
117
+ else
118
+ resources.first.class
119
+ end
120
+ end
121
+
122
+ def table_layout
123
+ selectable.present? ? 'md:table md:table-fixed' : 'md:table'
124
+ end
125
+
126
+ def resources_class_name
127
+ resource_class.to_s.parameterize(separator: '_')
128
+ end
129
+
130
+ def form
131
+ selectable[:form]
132
+ end
133
+
134
+ def form_check_box(resource)
135
+ form.check_box(resources_class_name, { multiple: true }, resource.id, nil)
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,23 @@
1
+ <div data-controller="f-table-component">
2
+ <%=
3
+ desktop_table(
4
+ columns: columns,
5
+ rows: rows,
6
+ actions: @actions,
7
+ check_boxes: @check_boxes,
8
+ **@options
9
+ )
10
+ %>
11
+
12
+ <%=
13
+ mobile_table(
14
+ columns: columns,
15
+ rows: rows,
16
+ main_column: @main_column,
17
+ actions: @actions,
18
+ check_boxes: @check_boxes,
19
+ resources: @resources,
20
+ **@options
21
+ )
22
+ %>
23
+ </div>
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FComponents
4
+ module Table
5
+ class Component < Base
6
+ renders_one :desktop_table, Table::Desktop::Component
7
+ renders_one :mobile_table, Table::Mobile::Component
8
+
9
+ def initialize(options)
10
+ @options = options
11
+ @values = {}
12
+ @actions = []
13
+ @check_boxes = []
14
+ @main_column = nil
15
+ @current_resource = nil
16
+ @current_resource_index = 0
17
+ end
18
+
19
+ def self.for(resources, **options, &block)
20
+ table = new(options)
21
+ table.build(resources, &block)
22
+
23
+ table
24
+ end
25
+
26
+ def column(name, id: nil, main: false)
27
+ if main
28
+ @main_column = main.is_a?(Proc) ? main : name
29
+ end
30
+
31
+ @values[name] ||= []
32
+
33
+ return if @current_resource.blank?
34
+
35
+ @values[name] << { id: id, column_index: @current_resource_index, cell_content: yield(@current_resource).to_s }
36
+ end
37
+
38
+ def check_box(name:, value:, checked: false, **options)
39
+ value = value.(@current_resource) if value.respond_to? :call
40
+ checked = checked.(@current_resource) if checked.respond_to? :call
41
+
42
+ parsed_options = options
43
+
44
+ options.each do |k, v|
45
+ parsed_options[k] = v.(@current_resource) if v.respond_to? :call
46
+ end
47
+
48
+ parsed_options[:id] ||= "#{sanitize_to_id(name)}#{value}"
49
+
50
+ @check_boxes << check_box_tag(name, value, checked, **parsed_options)
51
+ end
52
+
53
+ def action
54
+ action = yield(@current_resource)
55
+
56
+ @actions[@current_resource_index] ||= []
57
+ @actions[@current_resource_index] << action
58
+ end
59
+
60
+ def build(resources)
61
+ @resources = resources.presence || [nil]
62
+ @resources.each_with_index do |resource, i|
63
+ @current_resource = resource
64
+ @current_resource_index = i
65
+
66
+ yield(self, @current_resource)
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ def columns
73
+ @values.keys
74
+ end
75
+
76
+ def rows
77
+ @values.values.transpose
78
+ end
79
+
80
+ # This was copied from Rails, since it's not a public API
81
+ # Source:
82
+ # https://github.com/rails/rails/blob/d305be07428a8e86e2736b57f839680c9e970293/actionview/lib/action_view/helpers/form_tag_helper.rb#L931
83
+ def sanitize_to_id(name)
84
+ name.to_s.delete(']').tr('^-a-zA-Z0-9:.', '_')
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,84 @@
1
+ import { Controller as BaseController } from '@hotwired/stimulus';
2
+ import { findAll } from '@fretadao/f-js-dom';
3
+
4
+ export default class extends BaseController {
5
+ static targets = ['desktopTable', 'mobileTable'];
6
+
7
+ connect() {
8
+ this.toggleDuplicatesHandler = this.toggleDuplicates.bind(this);
9
+
10
+ this.checkboxes().forEach(checkbox => {
11
+ checkbox.addEventListener('change', this.toggleDuplicatesHandler);
12
+ });
13
+
14
+
15
+ this.syncInputHandler = this.syncInput.bind(this)
16
+
17
+ this.inputs().forEach(input => {
18
+ input.addEventListener('input', this.syncInputHandler);
19
+ })
20
+
21
+ document.dispatchEvent(this.#loadedEvent());
22
+ }
23
+
24
+ disconnect() {
25
+ this.checkboxes().forEach(checkbox => {
26
+ checkbox.removeEventListener('change', this.toggleDuplicatesHandler);
27
+ });
28
+
29
+ this.inputs().forEach(input => {
30
+ input.removeEventListener('change', this.syncInputHandler);
31
+ })
32
+ }
33
+
34
+ toggleDuplicates(event) {
35
+ const checkbox = event.currentTarget;
36
+
37
+ this.checkboxes({ id: checkbox.id, value: checkbox.value }).forEach(dup => {
38
+ dup.checked = checkbox.checked;
39
+ });
40
+
41
+ this.element.dispatchEvent(new CustomEvent('checkboxesChanged', { bubbles: true }));
42
+ }
43
+
44
+ syncInput(event) {
45
+ const input = event.currentTarget;
46
+
47
+ this.inputs(input.id).forEach(dup => {
48
+ dup.value = input.value;
49
+ });
50
+
51
+ this.element.dispatchEvent(new CustomEvent('inputsChanged', { bubbles: true }));
52
+ }
53
+
54
+ checkboxes({ id, value } = {}) {
55
+ let query = 'input[type=checkbox]';
56
+
57
+ if(!!id) {
58
+ query = `#${id}`;
59
+ } else if (!!value) {
60
+ query += `[value="${value}"]`;
61
+ }
62
+
63
+ return findAll(query, this.element);
64
+ }
65
+
66
+ inputs(id = null) {
67
+ let query = 'input:not([type=checkbox])';
68
+
69
+ if(!!id) {
70
+ query = `#${id}`;
71
+ }
72
+
73
+ return findAll(query, this.element);
74
+ }
75
+
76
+ #loadedEvent() {
77
+ return new CustomEvent(
78
+ 'f-components-table:loaded',
79
+ {
80
+ detail: { desktopTable: this.desktopTableTarget, mobileTable: this.mobileTableTarget }
81
+ }
82
+ );
83
+ }
84
+ }
@@ -0,0 +1,25 @@
1
+ <%= tag.table(id: @id, class: "hidden md:w-full md:table md:text-sm #{@class}", **@options) do %>
2
+ <thead>
3
+ <tr class="text-left border-b-2 border-gray-lt">
4
+ <% @columns.each do |header| %>
5
+ <th class='px-4 py-3'><%= header %></th>
6
+ <% end %>
7
+ </tr>
8
+ </thead>
9
+
10
+ <tbody>
11
+ <% @rows.each_with_index do |row, i| %>
12
+ <tr class='even:bg-gray-ltr'>
13
+ <%= check_box_for(resource_index: i) %>
14
+
15
+ <% row.each do |cell| %>
16
+ <td class="px-4 py-3">
17
+ <%= format_cell(cell) %>
18
+ </td>
19
+ <% end %>
20
+
21
+ <%= actions(resource_index: i) %>
22
+ </tr>
23
+ <% end %>
24
+ </tbody>
25
+ <% end %>
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FComponents
4
+ module Table
5
+ module Desktop
6
+ class Component < Base
7
+ def initialize(**options)
8
+ @rows = options.delete(:rows)
9
+ @actions = options.delete(:actions)
10
+ @check_boxes = options.delete(:check_boxes)
11
+ @columns = options.delete(:columns)
12
+ @columns.prepend('') if @check_boxes.present?
13
+ @columns.push('') if @actions.present?
14
+ @id = ['desktop-table', options.delete(:id_suffix)].filter_map(&:presence).join('-')
15
+ @class = options.delete(:class)
16
+ add_target(options)
17
+ @options = options
18
+ end
19
+
20
+ def check_box_for(resource_index:)
21
+ return if @check_boxes.empty?
22
+
23
+ tag.td(class: 'py-3 text-center') { @check_boxes[resource_index] }
24
+ end
25
+
26
+ def actions(resource_index:)
27
+ return if @actions.blank?
28
+
29
+ resource_actions = @actions[resource_index]
30
+
31
+ tag.td(class: 'py-3 px-4') do
32
+ actions_for_resource(resource_actions)
33
+ end
34
+ end
35
+
36
+ def actions_for_resource(resource_actions)
37
+ fcomponent :dropdown, label: 'Opções', padding: false, icon: 'plus', class: 'w-fit mx-auto' do
38
+ safe_join(
39
+ resource_actions.map do |action|
40
+ tag.div(class: 'text-sm py-3 px-5 even:bg-gray-lt') do
41
+ action
42
+ end
43
+ end
44
+ )
45
+ end
46
+ end
47
+
48
+ def format_cell(value)
49
+ cell_content = value.is_a?(Hash) ? value[:cell_content] : value
50
+ return cell_content if cell_content.present? && html?(cell_content)
51
+
52
+ cell_id = value[:id].present? ? "#{value[:id]}-desktop-#{value[:column_index]}" : nil
53
+
54
+ tag.p(id: cell_id, class: 'max-w-5/6') { cell_content }
55
+ end
56
+
57
+ def html?(test_target)
58
+ Nokogiri::XML.parse(test_target.to_s).errors.empty?
59
+ end
60
+
61
+ def add_target(options)
62
+ options ||= {}
63
+ options[:data] ||= {}
64
+ options[:data][:f_table_component_target] = 'desktopTable'
65
+
66
+ nil
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,8 @@
1
+ <%= tag.div(id: @id, class: "md:hidden #{@class}", **@options) do %>
2
+ <% rows.each_with_index do |row, i| %>
3
+ <%= table_head(resource_index: i) do %>
4
+ <%= table_body(row) %>
5
+ <%= actions(resource_index: i) %>
6
+ <% end %>
7
+ <% end %>
8
+ <% end %>
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FComponents
4
+ module Table
5
+ module Mobile
6
+ class Component < Base
7
+ def initialize(**options)
8
+ @rows = options.delete(:rows)
9
+ @actions = options.delete(:actions)
10
+ @main_column = options.delete(:main_column)
11
+ @check_boxes = options.delete(:check_boxes)
12
+ @columns = options.delete(:columns)
13
+ @id = ['mobile-table', options.delete(:id_suffix)].filter_map(&:presence).join('-')
14
+ @class = options.delete(:class)
15
+ @resources = options.delete(:resources)
16
+ add_target(options)
17
+ @options = options
18
+ end
19
+
20
+ def table_head(resource_index:, &block)
21
+ current_resource = @resources[resource_index]
22
+
23
+ if @main_column.is_a?(Proc) && current_resource.present?
24
+ main_header = @main_column.call(current_resource)
25
+ else
26
+ main_header_index = @columns.index(@main_column) or invalid_column_error!
27
+ main_header = tag.strong(class: 'text-sm') do
28
+ value = @rows[resource_index][main_header_index]
29
+
30
+ value.is_a?(Hash) ? value[:cell_content] : value
31
+ end
32
+ end
33
+
34
+ title = if @check_boxes.present?
35
+ @check_boxes[resource_index] + main_header
36
+ else
37
+ main_header
38
+ end
39
+
40
+ fcomponent(:collapsible, summary: title, class: 'bg-white border-b border-gray-lt', &block)
41
+ end
42
+
43
+ def rows
44
+ @rows.map do |row|
45
+ row.map { |cell| format_cell(cell) }
46
+ end
47
+ end
48
+
49
+ def table_body(row)
50
+ tag.div class: 'flex flex-col space-y-5 mt-5' do
51
+ safe_join(
52
+ row.each_with_index.map do |value, position|
53
+ tag.div(class: 'flex items-center text-sm', data: { mobile_col: true }) do
54
+ tag.strong(class: 'w-20 min-w-20 mr-8') { @columns[position] } + format_cell(value)
55
+ end
56
+ end
57
+ )
58
+ end
59
+ end
60
+
61
+ def actions(resource_index:)
62
+ return if @actions.blank?
63
+
64
+ fcomponent :dropdown, label: 'Opções', padding: false, icon: 'plus', class: 'mt-5' do
65
+ safe_join(
66
+ @actions[resource_index].map do |action|
67
+ tag.div(class: 'text-sm py-3 px-5 even:bg-gray-lt') do
68
+ action
69
+ end
70
+ end
71
+ )
72
+ end
73
+ end
74
+
75
+ def format_cell(value)
76
+ cell_content = value.is_a?(Hash) ? value[:cell_content] : value
77
+ return cell_content if cell_content.present? && html?(cell_content)
78
+
79
+ cell_id = value[:id].present? ? "#{value[:id]}-mobile-#{value[:column_index]}" : nil
80
+
81
+ tag.p(id: cell_id, class: 'max-w-5/6') { cell_content }
82
+ end
83
+
84
+ def html?(test_target)
85
+ Nokogiri::XML.parse(test_target.to_s).errors.empty?
86
+ end
87
+
88
+ def invalid_column_error!
89
+ raise "Main column '#{@main_column}' not found in #{@columns}. Maybe you forgot to specify a main column?"
90
+ end
91
+
92
+ def add_target(options)
93
+ options ||= {}
94
+ options[:data] ||= {}
95
+ options[:data][:f_table_component_target] = 'mobileTable'
96
+
97
+ nil
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end