better_page 2.0.0
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 +7 -0
- data/CHANGELOG.md +62 -0
- data/MIT-LICENSE +20 -0
- data/README.md +357 -0
- data/Rakefile +3 -0
- data/docs/00-README.md +17 -0
- data/docs/01-getting-started.md +137 -0
- data/docs/02-component-registry.md +192 -0
- data/docs/03-base-pages.md +238 -0
- data/docs/04-schema-validation.md +180 -0
- data/docs/05-turbo-support.md +220 -0
- data/docs/06-compliance-analyzer.md +147 -0
- data/docs/07-configuration.md +157 -0
- data/guide/00-README.md +32 -0
- data/guide/01-quick-start.md +148 -0
- data/guide/02-building-index-page.md +258 -0
- data/guide/03-building-show-page.md +266 -0
- data/guide/04-building-form-page.md +309 -0
- data/guide/05-custom-pages.md +325 -0
- data/guide/06-best-practices.md +311 -0
- data/lib/better_page/base_page.rb +161 -0
- data/lib/better_page/compliance/analyzer.rb +409 -0
- data/lib/better_page/component_registry.rb +393 -0
- data/lib/better_page/config.rb +165 -0
- data/lib/better_page/configuration.rb +153 -0
- data/lib/better_page/custom_base_page.rb +85 -0
- data/lib/better_page/default_components.rb +200 -0
- data/lib/better_page/form_base_page.rb +170 -0
- data/lib/better_page/index_base_page.rb +69 -0
- data/lib/better_page/railtie.rb +34 -0
- data/lib/better_page/show_base_page.rb +120 -0
- data/lib/better_page/validation_error.rb +7 -0
- data/lib/better_page/version.rb +3 -0
- data/lib/better_page.rb +80 -0
- data/lib/generators/better_page/component_generator.rb +131 -0
- data/lib/generators/better_page/install_generator.rb +160 -0
- data/lib/generators/better_page/page_generator.rb +101 -0
- data/lib/generators/better_page/sync_generator.rb +109 -0
- data/lib/generators/better_page/templates/application_page.rb.tt +12 -0
- data/lib/generators/better_page/templates/better_page_initializer.rb.tt +53 -0
- data/lib/generators/better_page/templates/custom_base_page.rb.tt +83 -0
- data/lib/generators/better_page/templates/custom_page.rb.tt +31 -0
- data/lib/generators/better_page/templates/edit_page.rb.tt +46 -0
- data/lib/generators/better_page/templates/form_base_page.rb.tt +126 -0
- data/lib/generators/better_page/templates/index_base_page.rb.tt +65 -0
- data/lib/generators/better_page/templates/index_page.rb.tt +56 -0
- data/lib/generators/better_page/templates/javascript/controllers/app_nav_controller.js +57 -0
- data/lib/generators/better_page/templates/javascript/controllers/drawer_controller.js +99 -0
- data/lib/generators/better_page/templates/javascript/controllers/dropdown_controller.js +60 -0
- data/lib/generators/better_page/templates/javascript/controllers/index.js +36 -0
- data/lib/generators/better_page/templates/javascript/controllers/modal_controller.js +70 -0
- data/lib/generators/better_page/templates/javascript/controllers/sidebar_controller.js +152 -0
- data/lib/generators/better_page/templates/javascript/controllers/table_controller.js +60 -0
- data/lib/generators/better_page/templates/javascript/controllers/tabs_controller.js +89 -0
- data/lib/generators/better_page/templates/new_page.rb.tt +46 -0
- data/lib/generators/better_page/templates/show_base_page.rb.tt +117 -0
- data/lib/generators/better_page/templates/show_page.rb.tt +45 -0
- data/lib/generators/better_page/templates/view_components/application_view_component.rb.tt +7 -0
- data/lib/generators/better_page/templates/view_components/custom_view_component.html.erb.tt +21 -0
- data/lib/generators/better_page/templates/view_components/custom_view_component.rb.tt +21 -0
- data/lib/generators/better_page/templates/view_components/form_view_component.html.erb.tt +25 -0
- data/lib/generators/better_page/templates/view_components/form_view_component.rb.tt +23 -0
- data/lib/generators/better_page/templates/view_components/index_view_component.html.erb.tt +33 -0
- data/lib/generators/better_page/templates/view_components/index_view_component.rb.tt +29 -0
- data/lib/generators/better_page/templates/view_components/show_view_component.html.erb.tt +29 -0
- data/lib/generators/better_page/templates/view_components/show_view_component.rb.tt +25 -0
- data/lib/generators/better_page/templates/view_components/ui/alerts_component.html.erb.tt +47 -0
- data/lib/generators/better_page/templates/view_components/ui/alerts_component.rb.tt +47 -0
- data/lib/generators/better_page/templates/view_components/ui/content_section_component.html.erb.tt +42 -0
- data/lib/generators/better_page/templates/view_components/ui/content_section_component.rb.tt +34 -0
- data/lib/generators/better_page/templates/view_components/ui/drawer_component.html.erb.tt +73 -0
- data/lib/generators/better_page/templates/view_components/ui/drawer_component.rb.tt +78 -0
- data/lib/generators/better_page/templates/view_components/ui/errors_component.html.erb.tt +23 -0
- data/lib/generators/better_page/templates/view_components/ui/errors_component.rb.tt +18 -0
- data/lib/generators/better_page/templates/view_components/ui/field_component.html.erb.tt +65 -0
- data/lib/generators/better_page/templates/view_components/ui/field_component.rb.tt +91 -0
- data/lib/generators/better_page/templates/view_components/ui/footer_component.html.erb.tt +33 -0
- data/lib/generators/better_page/templates/view_components/ui/footer_component.rb.tt +32 -0
- data/lib/generators/better_page/templates/view_components/ui/header_component.html.erb.tt +55 -0
- data/lib/generators/better_page/templates/view_components/ui/header_component.rb.tt +39 -0
- data/lib/generators/better_page/templates/view_components/ui/modal_component.html.erb.tt +70 -0
- data/lib/generators/better_page/templates/view_components/ui/modal_component.rb.tt +54 -0
- data/lib/generators/better_page/templates/view_components/ui/overview_component.html.erb.tt +22 -0
- data/lib/generators/better_page/templates/view_components/ui/overview_component.rb.tt +71 -0
- data/lib/generators/better_page/templates/view_components/ui/pagination_component.html.erb.tt +63 -0
- data/lib/generators/better_page/templates/view_components/ui/pagination_component.rb.tt +69 -0
- data/lib/generators/better_page/templates/view_components/ui/panel_component.html.erb.tt +31 -0
- data/lib/generators/better_page/templates/view_components/ui/panel_component.rb.tt +23 -0
- data/lib/generators/better_page/templates/view_components/ui/statistics_component.html.erb.tt +33 -0
- data/lib/generators/better_page/templates/view_components/ui/statistics_component.rb.tt +51 -0
- data/lib/generators/better_page/templates/view_components/ui/table_component.html.erb.tt +112 -0
- data/lib/generators/better_page/templates/view_components/ui/table_component.rb.tt +88 -0
- data/lib/generators/better_page/templates/view_components/ui/tabs_component.html.erb.tt +52 -0
- data/lib/generators/better_page/templates/view_components/ui/tabs_component.rb.tt +76 -0
- data/lib/generators/better_page/templates/view_components/ui/widget_component.html.erb.tt +72 -0
- data/lib/generators/better_page/templates/view_components/ui/widget_component.rb.tt +34 -0
- data/lib/tasks/better_page.rake +70 -0
- data/lib/tasks/better_page_tasks.rake +4 -0
- metadata +188 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterPage
|
|
4
|
+
module Ui
|
|
5
|
+
class FooterComponent < BetterPage::ApplicationViewComponent
|
|
6
|
+
def initialize(actions: [], info: nil)
|
|
7
|
+
@actions = actions
|
|
8
|
+
@info = info
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
attr_reader :actions, :info
|
|
12
|
+
|
|
13
|
+
def actions? = actions.any?
|
|
14
|
+
def info? = info.present?
|
|
15
|
+
|
|
16
|
+
def action_classes(style)
|
|
17
|
+
base = "inline-flex items-center px-4 py-2 border text-sm font-medium rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2"
|
|
18
|
+
|
|
19
|
+
case style&.to_sym
|
|
20
|
+
when :primary
|
|
21
|
+
"#{base} border-transparent text-white bg-blue-600 hover:bg-blue-700 focus:ring-blue-500"
|
|
22
|
+
when :secondary
|
|
23
|
+
"#{base} border-gray-300 text-gray-700 bg-white hover:bg-gray-50 focus:ring-blue-500"
|
|
24
|
+
when :danger
|
|
25
|
+
"#{base} border-transparent text-white bg-red-600 hover:bg-red-700 focus:ring-red-500"
|
|
26
|
+
else
|
|
27
|
+
"#{base} border-gray-300 text-gray-700 bg-white hover:bg-gray-50 focus:ring-blue-500"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<header class="bg-white shadow rounded-xl p-4">
|
|
2
|
+
<%% if breadcrumbs? %>
|
|
3
|
+
<nav class="flex mb-4" aria-label="Breadcrumb">
|
|
4
|
+
<ol class="inline-flex items-center space-x-1 md:space-x-3">
|
|
5
|
+
<%% breadcrumbs.each_with_index do |crumb, i| %>
|
|
6
|
+
<li class="inline-flex items-center">
|
|
7
|
+
<%% if crumb[:path] %>
|
|
8
|
+
<%%= link_to crumb[:label], crumb[:path], class: "text-sm text-gray-500 hover:text-gray-700" %>
|
|
9
|
+
<%% else %>
|
|
10
|
+
<span class="text-sm font-medium text-gray-500"><%%= crumb[:label] %></span>
|
|
11
|
+
<%% end %>
|
|
12
|
+
<%% unless i == breadcrumbs.size - 1 %>
|
|
13
|
+
<svg class="w-4 h-4 mx-2 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
|
|
14
|
+
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"></path>
|
|
15
|
+
</svg>
|
|
16
|
+
<%% end %>
|
|
17
|
+
</li>
|
|
18
|
+
<%% end %>
|
|
19
|
+
</ol>
|
|
20
|
+
</nav>
|
|
21
|
+
<%% end %>
|
|
22
|
+
|
|
23
|
+
<div class="flex items-center justify-between">
|
|
24
|
+
<div>
|
|
25
|
+
<h1 class="text-2xl font-bold text-gray-900"><%%= title %></h1>
|
|
26
|
+
<%% if metadata? %>
|
|
27
|
+
<div class="mt-1 flex items-center space-x-4">
|
|
28
|
+
<%% metadata.each do |meta| %>
|
|
29
|
+
<span class="text-sm text-gray-500">
|
|
30
|
+
<%% if meta[:icon] %>
|
|
31
|
+
<span class="mr-1"><%%= meta[:icon] %></span>
|
|
32
|
+
<%% end %>
|
|
33
|
+
<%%= meta[:value] %>
|
|
34
|
+
</span>
|
|
35
|
+
<%% end %>
|
|
36
|
+
</div>
|
|
37
|
+
<%% end %>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<%% if actions? %>
|
|
41
|
+
<div class="flex space-x-3">
|
|
42
|
+
<%% actions.each do |action| %>
|
|
43
|
+
<%%= link_to action[:path],
|
|
44
|
+
data: { turbo_method: action[:method], turbo_confirm: action[:confirm] }.compact,
|
|
45
|
+
class: action_classes(action[:style]) do %>
|
|
46
|
+
<%% if action[:icon] %>
|
|
47
|
+
<span class="mr-2"><%%= action[:icon] %></span>
|
|
48
|
+
<%% end %>
|
|
49
|
+
<%%= action[:label] %>
|
|
50
|
+
<%% end %>
|
|
51
|
+
<%% end %>
|
|
52
|
+
</div>
|
|
53
|
+
<%% end %>
|
|
54
|
+
</div>
|
|
55
|
+
</header>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterPage
|
|
4
|
+
module Ui
|
|
5
|
+
class HeaderComponent < BetterPage::ApplicationViewComponent
|
|
6
|
+
def initialize(title:, breadcrumbs: [], actions: [], metadata: [])
|
|
7
|
+
@title = title
|
|
8
|
+
@breadcrumbs = breadcrumbs
|
|
9
|
+
@actions = actions
|
|
10
|
+
@metadata = metadata
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
attr_reader :title, :breadcrumbs, :actions, :metadata
|
|
14
|
+
|
|
15
|
+
def breadcrumbs? = breadcrumbs.any?
|
|
16
|
+
def actions? = actions.any?
|
|
17
|
+
def metadata? = metadata.any?
|
|
18
|
+
|
|
19
|
+
def action_classes(style)
|
|
20
|
+
base = "inline-flex items-center rounded-md px-3 py-2 text-sm font-semibold shadow-sm ring-1 ring-inset transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2"
|
|
21
|
+
|
|
22
|
+
case style&.to_sym
|
|
23
|
+
when :primary
|
|
24
|
+
"#{base} bg-blue-600 text-white ring-blue-600 hover:bg-blue-700 focus:ring-blue-500"
|
|
25
|
+
when :secondary
|
|
26
|
+
"#{base} bg-white text-gray-900 ring-gray-300 hover:bg-gray-50 focus:ring-blue-500"
|
|
27
|
+
when :danger
|
|
28
|
+
"#{base} bg-red-600 text-white ring-red-600 hover:bg-red-700 focus:ring-red-500"
|
|
29
|
+
when :success
|
|
30
|
+
"#{base} bg-green-600 text-white ring-green-600 hover:bg-green-700 focus:ring-green-500"
|
|
31
|
+
when :warning
|
|
32
|
+
"#{base} bg-yellow-600 text-white ring-yellow-600 hover:bg-yellow-700 focus:ring-yellow-500"
|
|
33
|
+
else
|
|
34
|
+
"#{base} bg-white text-gray-900 ring-gray-300 hover:bg-gray-50 focus:ring-blue-500"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<div id="<%%= id %>"
|
|
2
|
+
data-controller="modal"
|
|
3
|
+
data-modal-confirm-close-value="<%%= confirm_close %>">
|
|
4
|
+
|
|
5
|
+
<%%# Trigger slot - bottone per aprire il modal %>
|
|
6
|
+
<%% if trigger? %>
|
|
7
|
+
<%%= trigger %>
|
|
8
|
+
<%% end %>
|
|
9
|
+
|
|
10
|
+
<%%# Container del modal (nascosto di default) %>
|
|
11
|
+
<div data-modal-target="container"
|
|
12
|
+
class="hidden relative z-50"
|
|
13
|
+
aria-modal="true"
|
|
14
|
+
role="dialog">
|
|
15
|
+
|
|
16
|
+
<%%# Backdrop %>
|
|
17
|
+
<div data-modal-target="backdrop"
|
|
18
|
+
data-action="click->modal#backdropClick"
|
|
19
|
+
class="fixed inset-0 bg-black/50 backdrop-blur-sm transition-opacity duration-300 ease-out opacity-0">
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<%%# Modal panel container %>
|
|
23
|
+
<div class="fixed inset-0 z-10 w-screen overflow-y-auto">
|
|
24
|
+
<div class="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
|
25
|
+
<div data-modal-target="panel"
|
|
26
|
+
class="<%%= panel_classes %> opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95">
|
|
27
|
+
|
|
28
|
+
<%%# Header %>
|
|
29
|
+
<%% if show_header? %>
|
|
30
|
+
<div class="flex items-center justify-between px-4 py-3 border-b border-gray-200">
|
|
31
|
+
<div class="flex items-center gap-4">
|
|
32
|
+
<%% if title? %>
|
|
33
|
+
<h3 class="text-lg font-semibold leading-6 text-gray-900"><%%= title %></h3>
|
|
34
|
+
<%% end %>
|
|
35
|
+
<%% if header_actions? %>
|
|
36
|
+
<div class="flex items-center gap-2">
|
|
37
|
+
<%%= actions %>
|
|
38
|
+
</div>
|
|
39
|
+
<%% end %>
|
|
40
|
+
</div>
|
|
41
|
+
<%% if closable? %>
|
|
42
|
+
<button type="button"
|
|
43
|
+
data-action="click->modal#requestClose"
|
|
44
|
+
class="rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
45
|
+
<span class="sr-only">Close</span>
|
|
46
|
+
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
47
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
48
|
+
</svg>
|
|
49
|
+
</button>
|
|
50
|
+
<%% end %>
|
|
51
|
+
</div>
|
|
52
|
+
<%% end %>
|
|
53
|
+
|
|
54
|
+
<%%# Content %>
|
|
55
|
+
<div class="px-4 py-4">
|
|
56
|
+
<%%= content %>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<%%# Footer %>
|
|
60
|
+
<%% if footer_actions? %>
|
|
61
|
+
<div class="flex items-center justify-end gap-2 px-4 py-3 border-t border-gray-200 bg-gray-50">
|
|
62
|
+
<%%= actions %>
|
|
63
|
+
</div>
|
|
64
|
+
<%% end %>
|
|
65
|
+
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterPage
|
|
4
|
+
module Ui
|
|
5
|
+
class ModalComponent < BetterPage::ApplicationViewComponent
|
|
6
|
+
renders_one :trigger
|
|
7
|
+
renders_one :actions
|
|
8
|
+
|
|
9
|
+
SIZES = {
|
|
10
|
+
normal: "max-w-md",
|
|
11
|
+
large: "max-w-2xl"
|
|
12
|
+
}.freeze
|
|
13
|
+
|
|
14
|
+
def initialize(id:, title: nil, size: :normal, closable: true, actions_position: :footer, confirm_close: false)
|
|
15
|
+
@id = id
|
|
16
|
+
@title = title
|
|
17
|
+
@size = size.to_sym
|
|
18
|
+
@closable = closable
|
|
19
|
+
@actions_position = actions_position.to_sym
|
|
20
|
+
@confirm_close = confirm_close
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
attr_reader :id, :title, :size, :closable, :actions_position, :confirm_close
|
|
24
|
+
|
|
25
|
+
def closable?
|
|
26
|
+
@closable
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def title?
|
|
30
|
+
title.present?
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def show_header?
|
|
34
|
+
title? || closable?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def header_actions?
|
|
38
|
+
actions? && actions_position == :header
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def footer_actions?
|
|
42
|
+
actions? && actions_position == :footer
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def size_class
|
|
46
|
+
SIZES.fetch(size, SIZES[:normal])
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def panel_classes
|
|
50
|
+
"relative transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all sm:my-8 sm:w-full #{size_class}"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<%% if fields? %>
|
|
2
|
+
<div class="bg-white shadow overflow-hidden rounded-lg mb-6">
|
|
3
|
+
<div class="px-4 py-5 sm:p-6">
|
|
4
|
+
<dl class="grid grid-cols-1 gap-x-4 gap-y-8 <%%= grid_classes %>">
|
|
5
|
+
<%% fields.each do |field| %>
|
|
6
|
+
<div class="<%%= field[:span] ? "sm:col-span-#{field[:span]}" : '' %>">
|
|
7
|
+
<dt class="text-sm font-medium text-gray-500"><%%= field[:label] %></dt>
|
|
8
|
+
<dd class="mt-1 text-sm text-gray-900">
|
|
9
|
+
<%% if field[:format] == :badge && field[:color] %>
|
|
10
|
+
<span class="<%%= badge_classes(field[:color]) %>"><%%= format_value(field) %></span>
|
|
11
|
+
<%% elsif field[:link] %>
|
|
12
|
+
<%%= link_to format_value(field), field[:link], class: "text-blue-600 hover:text-blue-500" %>
|
|
13
|
+
<%% else %>
|
|
14
|
+
<%%= format_value(field) %>
|
|
15
|
+
<%% end %>
|
|
16
|
+
</dd>
|
|
17
|
+
</div>
|
|
18
|
+
<%% end %>
|
|
19
|
+
</dl>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
<%% end %>
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterPage
|
|
4
|
+
module Ui
|
|
5
|
+
class OverviewComponent < BetterPage::ApplicationViewComponent
|
|
6
|
+
def initialize(fields:, columns: 2)
|
|
7
|
+
@fields = fields
|
|
8
|
+
@columns = columns
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
attr_reader :fields, :columns
|
|
12
|
+
|
|
13
|
+
def fields? = fields.any?
|
|
14
|
+
|
|
15
|
+
def grid_classes
|
|
16
|
+
case columns
|
|
17
|
+
when 1 then "sm:grid-cols-1"
|
|
18
|
+
when 2 then "sm:grid-cols-2"
|
|
19
|
+
when 3 then "sm:grid-cols-3"
|
|
20
|
+
when 4 then "sm:grid-cols-4"
|
|
21
|
+
else "sm:grid-cols-2"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def format_value(field)
|
|
26
|
+
value = field[:value]
|
|
27
|
+
return field[:empty] || "-" if value.blank?
|
|
28
|
+
|
|
29
|
+
case field[:format]&.to_sym
|
|
30
|
+
when :currency
|
|
31
|
+
number_to_currency(value)
|
|
32
|
+
when :date
|
|
33
|
+
value.respond_to?(:strftime) ? value.strftime("%B %d, %Y") : value
|
|
34
|
+
when :datetime
|
|
35
|
+
value.respond_to?(:strftime) ? value.strftime("%B %d, %Y %H:%M") : value
|
|
36
|
+
when :boolean
|
|
37
|
+
value ? "Yes" : "No"
|
|
38
|
+
when :percentage
|
|
39
|
+
"#{value}%"
|
|
40
|
+
when :badge
|
|
41
|
+
value
|
|
42
|
+
else
|
|
43
|
+
value
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def badge_classes(color)
|
|
48
|
+
base = "inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"
|
|
49
|
+
|
|
50
|
+
case color&.to_sym
|
|
51
|
+
when :green
|
|
52
|
+
"#{base} bg-green-100 text-green-800"
|
|
53
|
+
when :red
|
|
54
|
+
"#{base} bg-red-100 text-red-800"
|
|
55
|
+
when :yellow
|
|
56
|
+
"#{base} bg-yellow-100 text-yellow-800"
|
|
57
|
+
when :blue
|
|
58
|
+
"#{base} bg-blue-100 text-blue-800"
|
|
59
|
+
when :blue
|
|
60
|
+
"#{base} bg-blue-100 text-blue-800"
|
|
61
|
+
when :purple
|
|
62
|
+
"#{base} bg-purple-100 text-purple-800"
|
|
63
|
+
when :pink
|
|
64
|
+
"#{base} bg-pink-100 text-pink-800"
|
|
65
|
+
else
|
|
66
|
+
"#{base} bg-gray-100 text-gray-800"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<%% if paginated? || show_info? %>
|
|
2
|
+
<nav class="flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6" aria-label="Pagination">
|
|
3
|
+
<%% if show_info? %>
|
|
4
|
+
<div class="hidden sm:block">
|
|
5
|
+
<p class="text-sm text-gray-700">
|
|
6
|
+
Showing
|
|
7
|
+
<span class="font-medium"><%%= from_count %></span>
|
|
8
|
+
to
|
|
9
|
+
<span class="font-medium"><%%= to_count %></span>
|
|
10
|
+
of
|
|
11
|
+
<span class="font-medium"><%%= total_count %></span>
|
|
12
|
+
results
|
|
13
|
+
</p>
|
|
14
|
+
</div>
|
|
15
|
+
<%% end %>
|
|
16
|
+
|
|
17
|
+
<%% if paginated? %>
|
|
18
|
+
<div class="flex flex-1 justify-between sm:justify-end">
|
|
19
|
+
<div class="isolate inline-flex -space-x-px rounded-md shadow-sm">
|
|
20
|
+
<%% if current_page > 1 %>
|
|
21
|
+
<%%= link_to url_for(page: current_page - 1), class: "relative inline-flex items-center rounded-l-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-20 focus:outline-offset-0" do %>
|
|
22
|
+
<span class="sr-only">Previous</span>
|
|
23
|
+
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
|
24
|
+
<path fill-rule="evenodd" d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z" clip-rule="evenodd" />
|
|
25
|
+
</svg>
|
|
26
|
+
<%% end %>
|
|
27
|
+
<%% else %>
|
|
28
|
+
<span class="relative inline-flex items-center rounded-l-md px-2 py-2 text-gray-300 ring-1 ring-inset ring-gray-300 cursor-not-allowed">
|
|
29
|
+
<span class="sr-only">Previous</span>
|
|
30
|
+
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
|
31
|
+
<path fill-rule="evenodd" d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z" clip-rule="evenodd" />
|
|
32
|
+
</svg>
|
|
33
|
+
</span>
|
|
34
|
+
<%% end %>
|
|
35
|
+
|
|
36
|
+
<%% page_numbers.each do |page| %>
|
|
37
|
+
<%% if page == :ellipsis %>
|
|
38
|
+
<span class="relative inline-flex items-center px-4 py-2 text-sm font-semibold text-gray-700 ring-1 ring-inset ring-gray-300">...</span>
|
|
39
|
+
<%% else %>
|
|
40
|
+
<%%= link_to page, url_for(page: page), class: page_link_classes(page) %>
|
|
41
|
+
<%% end %>
|
|
42
|
+
<%% end %>
|
|
43
|
+
|
|
44
|
+
<%% if current_page < total_pages %>
|
|
45
|
+
<%%= link_to url_for(page: current_page + 1), class: "relative inline-flex items-center rounded-r-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-20 focus:outline-offset-0" do %>
|
|
46
|
+
<span class="sr-only">Next</span>
|
|
47
|
+
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
|
48
|
+
<path fill-rule="evenodd" d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" clip-rule="evenodd" />
|
|
49
|
+
</svg>
|
|
50
|
+
<%% end %>
|
|
51
|
+
<%% else %>
|
|
52
|
+
<span class="relative inline-flex items-center rounded-r-md px-2 py-2 text-gray-300 ring-1 ring-inset ring-gray-300 cursor-not-allowed">
|
|
53
|
+
<span class="sr-only">Next</span>
|
|
54
|
+
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
|
55
|
+
<path fill-rule="evenodd" d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" clip-rule="evenodd" />
|
|
56
|
+
</svg>
|
|
57
|
+
</span>
|
|
58
|
+
<%% end %>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
<%% end %>
|
|
62
|
+
</nav>
|
|
63
|
+
<%% end %>
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterPage
|
|
4
|
+
module Ui
|
|
5
|
+
class PaginationComponent < BetterPage::ApplicationViewComponent
|
|
6
|
+
def initialize(items:, info: true)
|
|
7
|
+
@items = items
|
|
8
|
+
@show_info = info
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
attr_reader :items
|
|
12
|
+
|
|
13
|
+
def show_info? = @show_info
|
|
14
|
+
def paginated? = items.respond_to?(:total_pages) && items.total_pages > 1
|
|
15
|
+
|
|
16
|
+
def current_page = items.respond_to?(:current_page) ? items.current_page : 1
|
|
17
|
+
def total_pages = items.respond_to?(:total_pages) ? items.total_pages : 1
|
|
18
|
+
def total_count = items.respond_to?(:total_count) ? items.total_count : items.size
|
|
19
|
+
|
|
20
|
+
def from_count
|
|
21
|
+
return 0 if total_count.zero?
|
|
22
|
+
|
|
23
|
+
per_page = items.respond_to?(:limit_value) ? items.limit_value : 25
|
|
24
|
+
((current_page - 1) * per_page) + 1
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def to_count
|
|
28
|
+
per_page = items.respond_to?(:limit_value) ? items.limit_value : 25
|
|
29
|
+
[current_page * per_page, total_count].min
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def page_numbers
|
|
33
|
+
return [] unless paginated?
|
|
34
|
+
|
|
35
|
+
pages = []
|
|
36
|
+
window = 2
|
|
37
|
+
|
|
38
|
+
# Always show first page
|
|
39
|
+
pages << 1
|
|
40
|
+
|
|
41
|
+
# Add ellipsis if needed
|
|
42
|
+
pages << :ellipsis if current_page > window + 2
|
|
43
|
+
|
|
44
|
+
# Add pages around current
|
|
45
|
+
((current_page - window)..(current_page + window)).each do |page|
|
|
46
|
+
pages << page if page > 1 && page < total_pages
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Add ellipsis if needed
|
|
50
|
+
pages << :ellipsis if current_page < total_pages - window - 1
|
|
51
|
+
|
|
52
|
+
# Always show last page
|
|
53
|
+
pages << total_pages if total_pages > 1
|
|
54
|
+
|
|
55
|
+
pages.uniq
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def page_link_classes(page)
|
|
59
|
+
base = "relative inline-flex items-center px-4 py-2 text-sm font-medium"
|
|
60
|
+
|
|
61
|
+
if page == current_page
|
|
62
|
+
"#{base} z-10 bg-blue-50 border-blue-500 text-blue-600"
|
|
63
|
+
else
|
|
64
|
+
"#{base} bg-white border-gray-300 text-gray-500 hover:bg-gray-50"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<div class="bg-white shadow rounded-lg overflow-hidden mb-6" <%% if collapsible? %>data-controller="panel"<%% end %>>
|
|
2
|
+
<%% if title? %>
|
|
3
|
+
<div class="px-4 py-5 sm:px-6 border-b border-gray-200 <%%= collapsible? ? 'cursor-pointer' : '' %>" <%% if collapsible? %>data-action="click->panel#toggle"<%% end %>>
|
|
4
|
+
<div class="flex items-center justify-between">
|
|
5
|
+
<div>
|
|
6
|
+
<h3 class="text-lg font-medium leading-6 text-gray-900"><%%= title %></h3>
|
|
7
|
+
<%% if description? %>
|
|
8
|
+
<p class="mt-1 text-sm text-gray-500"><%%= description %></p>
|
|
9
|
+
<%% end %>
|
|
10
|
+
</div>
|
|
11
|
+
<%% if collapsible? %>
|
|
12
|
+
<div class="ml-4 flex-shrink-0">
|
|
13
|
+
<svg class="h-5 w-5 text-gray-400 transition-transform" data-panel-target="icon" <%%= collapsed? ? '' : 'style="transform: rotate(180deg)"' %> fill="currentColor" viewBox="0 0 20 20">
|
|
14
|
+
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
|
|
15
|
+
</svg>
|
|
16
|
+
</div>
|
|
17
|
+
<%% end %>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
<%% end %>
|
|
21
|
+
|
|
22
|
+
<%% if fields? %>
|
|
23
|
+
<div class="px-4 py-5 sm:p-6 <%%= collapsed? ? 'hidden' : '' %>" <%% if collapsible? %>data-panel-target="content"<%% end %>>
|
|
24
|
+
<div class="grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6">
|
|
25
|
+
<%% fields.each do |field| %>
|
|
26
|
+
<%%= render BetterPage::Ui::FieldComponent.new(**field) %>
|
|
27
|
+
<%% end %>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
<%% end %>
|
|
31
|
+
</div>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterPage
|
|
4
|
+
module Ui
|
|
5
|
+
class PanelComponent < BetterPage::ApplicationViewComponent
|
|
6
|
+
def initialize(title: nil, description: nil, fields: [], collapsible: false, collapsed: false)
|
|
7
|
+
@title = title
|
|
8
|
+
@description = description
|
|
9
|
+
@fields = fields
|
|
10
|
+
@collapsible = collapsible
|
|
11
|
+
@collapsed = collapsed
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
attr_reader :title, :description, :fields, :collapsible, :collapsed
|
|
15
|
+
|
|
16
|
+
def title? = title.present?
|
|
17
|
+
def description? = description.present?
|
|
18
|
+
def fields? = fields.any?
|
|
19
|
+
def collapsible? = collapsible
|
|
20
|
+
def collapsed? = collapsed
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<%% if stats? %>
|
|
2
|
+
<div class="mb-6">
|
|
3
|
+
<dl class="grid gap-5 <%%= grid_classes %>">
|
|
4
|
+
<%% stats.each do |stat| %>
|
|
5
|
+
<div class="relative bg-white pt-5 px-4 pb-12 sm:pt-6 sm:px-6 shadow rounded-lg overflow-hidden">
|
|
6
|
+
<dt>
|
|
7
|
+
<%% if stat[:icon] %>
|
|
8
|
+
<div class="absolute bg-blue-500 rounded-md p-3">
|
|
9
|
+
<span class="h-6 w-6 text-white"><%%= stat[:icon] %></span>
|
|
10
|
+
</div>
|
|
11
|
+
<%% end %>
|
|
12
|
+
<p class="<%%= stat[:icon] ? 'ml-16' : '' %> text-sm font-medium text-gray-500 truncate"><%%= stat[:label] %></p>
|
|
13
|
+
</dt>
|
|
14
|
+
<dd class="<%%= stat[:icon] ? 'ml-16' : '' %> flex items-baseline pb-6 sm:pb-7">
|
|
15
|
+
<p class="text-2xl font-semibold text-gray-900"><%%= stat[:value] %></p>
|
|
16
|
+
<%% if stat[:change] %>
|
|
17
|
+
<p class="ml-2 flex items-baseline text-sm font-semibold <%%= trend_classes(stat[:trend]) %>">
|
|
18
|
+
<%%= trend_icon(stat[:trend]) %>
|
|
19
|
+
<span class="sr-only"><%%= stat[:trend] == :up ? 'Increased' : 'Decreased' %> by</span>
|
|
20
|
+
<%%= stat[:change] %>
|
|
21
|
+
</p>
|
|
22
|
+
<%% end %>
|
|
23
|
+
<%% if stat[:link] %>
|
|
24
|
+
<div class="absolute bottom-0 inset-x-0 bg-gray-50 px-4 py-4 sm:px-6">
|
|
25
|
+
<%%= link_to stat[:link][:label], stat[:link][:path], class: "text-sm font-medium text-blue-600 hover:text-blue-500" %>
|
|
26
|
+
</div>
|
|
27
|
+
<%% end %>
|
|
28
|
+
</dd>
|
|
29
|
+
</div>
|
|
30
|
+
<%% end %>
|
|
31
|
+
</dl>
|
|
32
|
+
</div>
|
|
33
|
+
<%% end %>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterPage
|
|
4
|
+
module Ui
|
|
5
|
+
class StatisticsComponent < BetterPage::ApplicationViewComponent
|
|
6
|
+
def initialize(stats:, columns: 4)
|
|
7
|
+
@stats = Array(stats)
|
|
8
|
+
@columns = columns
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
attr_reader :stats, :columns
|
|
12
|
+
|
|
13
|
+
def stats? = stats.any?
|
|
14
|
+
|
|
15
|
+
def grid_classes
|
|
16
|
+
case columns
|
|
17
|
+
when 2
|
|
18
|
+
"grid-cols-1 sm:grid-cols-2"
|
|
19
|
+
when 3
|
|
20
|
+
"grid-cols-1 sm:grid-cols-2 lg:grid-cols-3"
|
|
21
|
+
when 4
|
|
22
|
+
"grid-cols-1 sm:grid-cols-2 lg:grid-cols-4"
|
|
23
|
+
when 5
|
|
24
|
+
"grid-cols-1 sm:grid-cols-2 lg:grid-cols-5"
|
|
25
|
+
else
|
|
26
|
+
"grid-cols-1 sm:grid-cols-2 lg:grid-cols-4"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def trend_classes(trend)
|
|
31
|
+
case trend&.to_sym
|
|
32
|
+
when :up
|
|
33
|
+
"text-green-600"
|
|
34
|
+
when :down
|
|
35
|
+
"text-red-600"
|
|
36
|
+
else
|
|
37
|
+
"text-gray-500"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def trend_icon(trend)
|
|
42
|
+
case trend&.to_sym
|
|
43
|
+
when :up
|
|
44
|
+
'<svg class="h-5 w-5 text-green-500" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M5.293 9.707a1 1 0 010-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 01-1.414 1.414L11 7.414V15a1 1 0 11-2 0V7.414L6.707 9.707a1 1 0 01-1.414 0z" clip-rule="evenodd" /></svg>'.html_safe
|
|
45
|
+
when :down
|
|
46
|
+
'<svg class="h-5 w-5 text-red-500" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M14.707 10.293a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 111.414-1.414L9 12.586V5a1 1 0 012 0v7.586l2.293-2.293a1 1 0 011.414 0z" clip-rule="evenodd" /></svg>'.html_safe
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|