panda-core 0.1.16 → 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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +9 -16
  3. data/Rakefile +3 -0
  4. data/app/builders/panda/core/form_builder.rb +225 -0
  5. data/app/components/panda/core/admin/button_component.rb +70 -0
  6. data/app/components/panda/core/admin/container_component.html.erb +12 -0
  7. data/app/components/panda/core/admin/container_component.rb +13 -0
  8. data/app/components/panda/core/admin/flash_message_component.html.erb +31 -0
  9. data/app/components/panda/core/admin/flash_message_component.rb +47 -0
  10. data/app/components/panda/core/admin/heading_component.rb +46 -0
  11. data/app/components/panda/core/admin/panel_component.html.erb +7 -0
  12. data/app/components/panda/core/admin/panel_component.rb +13 -0
  13. data/app/components/panda/core/admin/slideover_component.html.erb +9 -0
  14. data/app/components/panda/core/admin/slideover_component.rb +15 -0
  15. data/app/components/panda/core/admin/table_component.html.erb +29 -0
  16. data/app/components/panda/core/admin/table_component.rb +46 -0
  17. data/app/components/panda/core/admin/tag_component.rb +35 -0
  18. data/app/constraints/panda/core/admin_constraint.rb +14 -0
  19. data/app/controllers/panda/core/admin/dashboard_controller.rb +22 -0
  20. data/app/controllers/panda/core/admin/my_profile_controller.rb +49 -0
  21. data/app/controllers/panda/core/admin/sessions_controller.rb +69 -0
  22. data/app/controllers/panda/core/admin_controller.rb +28 -0
  23. data/app/controllers/panda/core/application_controller.rb +59 -0
  24. data/app/helpers/panda/core/asset_helper.rb +32 -0
  25. data/app/javascript/panda/core/application.js +9 -0
  26. data/app/javascript/panda/core/controllers/index.js +20 -0
  27. data/app/javascript/panda/core/controllers/theme_form_controller.js +25 -0
  28. data/app/javascript/panda/core/tailwindcss-stimulus-components.js +3 -0
  29. data/app/models/panda/core/application_record.rb +9 -0
  30. data/app/models/panda/core/breadcrumb.rb +17 -0
  31. data/app/models/panda/core/current.rb +16 -0
  32. data/app/models/panda/core/user.rb +51 -0
  33. data/app/views/layouts/panda/core/admin.html.erb +59 -0
  34. data/app/views/panda/core/admin/dashboard/show.html.erb +27 -0
  35. data/app/views/panda/core/admin/my_profile/edit.html.erb +49 -0
  36. data/app/views/panda/core/admin/sessions/new.html.erb +38 -0
  37. data/app/views/panda/core/admin/shared/_breadcrumbs.html.erb +35 -0
  38. data/app/views/panda/core/admin/shared/_flash.html.erb +31 -0
  39. data/app/views/panda/core/admin/shared/_sidebar.html.erb +27 -0
  40. data/app/views/panda/core/admin/shared/_slideover.html.erb +33 -0
  41. data/config/routes.rb +22 -0
  42. data/db/migrate/20241210000003_add_current_theme_to_panda_core_users.rb +7 -0
  43. data/db/migrate/20250809000001_create_panda_core_users.rb +16 -0
  44. data/lib/generators/panda/core/dev_tools/USAGE +24 -0
  45. data/lib/generators/panda/core/dev_tools/templates/lefthook.yml +13 -0
  46. data/lib/generators/panda/core/dev_tools/templates/spec_support_panda_core_helpers.rb +18 -0
  47. data/lib/generators/panda/core/dev_tools_generator.rb +143 -0
  48. data/lib/panda/core/asset_loader.rb +221 -0
  49. data/lib/panda/core/authentication.rb +36 -0
  50. data/lib/panda/core/component_registry.rb +37 -0
  51. data/lib/panda/core/configuration.rb +31 -1
  52. data/lib/panda/core/engine.rb +43 -7
  53. data/lib/panda/core/notifications.rb +40 -0
  54. data/lib/panda/core/rake_tasks.rb +16 -0
  55. data/lib/panda/core/subscribers/authentication_subscriber.rb +61 -0
  56. data/lib/panda/core/testing/capybara_config.rb +70 -0
  57. data/lib/panda/core/testing/omniauth_helpers.rb +52 -0
  58. data/lib/panda/core/testing/rspec_config.rb +72 -0
  59. data/lib/panda/core/version.rb +1 -1
  60. data/lib/panda/core.rb +2 -8
  61. data/lib/tasks/assets.rake +423 -0
  62. data/lib/tasks/panda/core/migrations.rake +13 -0
  63. data/lib/tasks/panda_core.rake +52 -0
  64. metadata +320 -11
  65. data/db/migrate/20250121012333_logidze_install.rb +0 -577
  66. data/db/migrate/20250121012334_enable_hstore.rb +0 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fc34797acb112eaf3a36808d79575e2f070db2d7f16a084e99d4bda39807595a
4
- data.tar.gz: 231c75cefbff1c0cdf22585bdf63d60b868bf8bd3a2ecca056507616b6d90b7f
3
+ metadata.gz: ae9cc770f537f0190416f1dece9c54ff839015625d0145a1bd97b77cb667455a
4
+ data.tar.gz: e4f6286a4f7c6a20aa5c71ae5528f7203bcaf30ceddda2ea9c774bee6ffec66c
5
5
  SHA512:
6
- metadata.gz: 72bfa970356456551ec696c305b7f4b856b6998c1f0e9fb8c2146b1aefd4c75f2c95ead4dd6e86d1e9a5ebb2ced6b87a75a291cc87c285cd4bd4170c631c5db3
7
- data.tar.gz: 83fda836d081fedd12dbe3614f6cbd1f3ff4f5c41682369e0c4c3372263472bc69f27c7384b33cdab2c1a1820d16e4f3be21013bf61b54cc9d857eec4f5a7415
6
+ metadata.gz: fa96a0fbfa58aaba72b3006455558736337f5cdfc504abf38af03db0e5697b599059a5a5501bc58d8b64cbac826f01122eea56f6a3e221de09d2eebfd57c478d
7
+ data.tar.gz: b9b96c780035db586a4fc566512e67de2922a29cb551c28fd3d3af72c72dc2b8853ca8a55364aa2fb62d8cf34cd19976a4b7afb6ab611c276a6ee799c4a3c182
data/README.md CHANGED
@@ -2,7 +2,8 @@
2
2
 
3
3
  Core functionality shared between Panda Software gems:
4
4
 
5
- - Panda CMS (https://github.com/tastybamboo/panda_cms)
5
+ - [Panda CMS](https://github.com/tastybamboo/panda-cms)
6
+ - [Panda Editor](https://github.com/tastybamboo/panda-editor)
6
7
 
7
8
  ## Installation
8
9
 
@@ -117,22 +118,14 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/tastyb
117
118
 
118
119
  ## Releasing
119
120
 
120
- For e.g. v0.1.8, run the following commands:
121
+ Panda Core uses automated releases via GitHub Actions. When changes to `lib/panda/core/version.rb` are merged to the `main` branch:
121
122
 
122
- ```bash
123
- RELEASE_VERSION=0.1.8
124
- git checkout -b release/v$RELEASE_VERSION
125
- gem bump --no-commit --version $RELEASE_VERSION
126
- bundle update
127
- git commit -am "Release $RELEASE_VERSION"
128
- git tag -a $RELEASE_VERSION -m "Release $RELEASE_VERSION"
129
- git push origin release/v$RELEASE_VERSION
130
- gem release panda-core -v $RELEASE_VERSION
131
- git checkout main && git merge release/v$RELEASE_VERSION
132
- git push origin main
133
- git push origin :release/v$RELEASE_VERSION
134
- ```
123
+ 1. The CI workflow runs tests
124
+ 2. If tests pass, the auto-release workflow triggers
125
+ 3. A git tag is created automatically (e.g., `v0.2.1`)
126
+ 4. The gem is built and published to RubyGems
127
+ 5. A GitHub release is created with changelog
135
128
 
136
129
  ## License
137
130
 
138
- The gem is available as open source under the terms of the [BSD 3-Clause License](https://opensource.org/licenses/BSD-3-Clause).
131
+ Copyright 2024-2025 Otaina Limited. The gem is available as open source under the terms of the [BSD 3-Clause License](https://opensource.org/licenses/BSD-3-Clause).
data/Rakefile CHANGED
@@ -5,3 +5,6 @@ load "rails/tasks/engine.rake"
5
5
  load "rails/tasks/statistics.rake"
6
6
 
7
7
  require "bundler/gem_tasks"
8
+
9
+ # Load Panda Core rake tasks
10
+ Dir[File.expand_path("lib/tasks/*.rake", __dir__)].each { |f| load f }
@@ -0,0 +1,225 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ class FormBuilder < ActionView::Helpers::FormBuilder
6
+ include ActionView::Helpers::TagHelper
7
+ include ActionView::Helpers::FormTagHelper
8
+
9
+ def label(attribute, text = nil, options = {})
10
+ super(attribute, text, options.reverse_merge(class: label_styles))
11
+ end
12
+
13
+ def text_field(attribute, options = {})
14
+ if options.dig(:data, :prefix)
15
+ content_tag :div, class: container_styles do
16
+ label(attribute) + meta_text(options) +
17
+ content_tag(:div, class: "flex flex-grow") do
18
+ content_tag(:span,
19
+ class: "inline-flex items-center px-3 text-base border border-r-none rounded-s-md whitespace-nowrap break-keep") do
20
+ options.dig(:data, :prefix)
21
+ end +
22
+ super(attribute, options.reverse_merge(class: "#{input_styles_prefix} input-prefix rounded-l-none border-l-none"))
23
+ end + error_message(attribute)
24
+ end
25
+ else
26
+ content_tag :div, class: container_styles do
27
+ label(attribute) + meta_text(options) + super(attribute, options.reverse_merge(class: input_styles)) + error_message(attribute)
28
+ end
29
+ end
30
+ end
31
+
32
+ def email_field(method, options = {})
33
+ content_tag :div, class: container_styles do
34
+ label(method) + meta_text(options) + super(method, options.reverse_merge(class: input_styles)) + error_message(method)
35
+ end
36
+ end
37
+
38
+ def datetime_field(method, options = {})
39
+ content_tag :div, class: container_styles do
40
+ label(method) + meta_text(options) + super(method, options.reverse_merge(class: input_styles)) + error_message(method)
41
+ end
42
+ end
43
+
44
+ def text_area(method, options = {})
45
+ content_tag :div, class: container_styles do
46
+ label(method) + meta_text(options) + super(method, options.reverse_merge(class: input_styles)) + error_message(method)
47
+ end
48
+ end
49
+
50
+ def password_field(attribute, options = {})
51
+ content_tag :div, class: container_styles do
52
+ label(attribute) + meta_text(options) + super(attribute, options.reverse_merge(class: input_styles)) + error_message(attribute)
53
+ end
54
+ end
55
+
56
+ def select(method, choices = nil, options = {}, html_options = {})
57
+ content_tag :div, class: container_styles do
58
+ label(method) + meta_text(options) + super(method, choices, options, html_options.reverse_merge(class: select_styles)) + select_svg + error_message(method)
59
+ end
60
+ end
61
+
62
+ def collection_select(method, collection, value_method, text_method, options = {}, html_options = {})
63
+ content_tag :div, class: container_styles do
64
+ label(method) + meta_text(options) + super(method, collection, value_method, text_method, options, html_options.reverse_merge(class: input_styles)) + error_message(method)
65
+ end
66
+ end
67
+
68
+ def time_zone_select(method, priority_zones = nil, options = {}, html_options = {})
69
+ wrap_field(method, options) do
70
+ super(
71
+ method,
72
+ priority_zones,
73
+ options,
74
+ html_options.reverse_merge(class: select_styles)
75
+ )
76
+ end
77
+ end
78
+
79
+ def file_field(method, options = {})
80
+ content_tag :div, class: container_styles do
81
+ label(method) + meta_text(options) + super(method, options.reverse_merge(class: "file:rounded file:border-0 file:text-sm file:bg-white file:text-gray-500 hover:file:bg-gray-50 bg-white px-2.5 hover:bg-gray-50".concat(input_styles)))
82
+ end
83
+ end
84
+
85
+ def button(value = nil, options = {}, &block)
86
+ value ||= submit_default_value
87
+ options = options.dup
88
+
89
+ # Handle formmethod specially
90
+ if options[:formmethod] == "delete"
91
+ options[:name] = "_method"
92
+ options[:value] = "delete"
93
+ end
94
+
95
+ base_classes = [
96
+ "inline-flex items-center rounded-md",
97
+ "px-3 py-2",
98
+ "text-base font-semibold",
99
+ "shadow-sm"
100
+ ]
101
+
102
+ # Only add fa-circle-check for non-block buttons
103
+ base_classes << "fa-circle-check" unless block_given?
104
+
105
+ options[:class] = [
106
+ *base_classes,
107
+ options[:class]
108
+ ].compact.join(" ")
109
+
110
+ if block_given?
111
+ @template.button_tag(options, &block)
112
+ else
113
+ @template.button_tag(value, options)
114
+ end
115
+ end
116
+
117
+ def submit(value = nil, options = {})
118
+ value ||= submit_default_value
119
+
120
+ # Use the same style logic as ButtonComponent
121
+ action = object.persisted? ? :save : :create
122
+ button_classes = case action
123
+ when :save, :create
124
+ "text-white bg-green-600 hover:bg-green-700"
125
+ when :save_inactive
126
+ "text-white bg-gray-400"
127
+ when :secondary
128
+ "text-gray-700 border-2 border-gray-500 bg-transparent hover:bg-gray-100 transition-all"
129
+ else
130
+ "text-gray-700 border-2 border-gray-500 bg-transparent hover:bg-gray-100 transition-all"
131
+ end
132
+
133
+ # Combine with common button classes
134
+ classes = "inline-flex items-center rounded-md font-medium shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 px-3 py-2 #{button_classes}"
135
+
136
+ options[:class] = options[:class] ? "#{options[:class]} #{classes}" : classes
137
+
138
+ super
139
+ end
140
+
141
+ def check_box(method, options = {}, checked_value = "1", unchecked_value = "0")
142
+ content_tag :div, class: container_styles do
143
+ label(method) + meta_text(options) + super(method, options.reverse_merge(class: "border-gray-300 ml-2"), checked_value, unchecked_value)
144
+ end
145
+ end
146
+
147
+ def date_field(method, options = {})
148
+ content_tag :div, class: container_styles do
149
+ label(method) + meta_text(options) + super(method, options.reverse_merge(class: input_styles))
150
+ end
151
+ end
152
+
153
+ def meta_text(options)
154
+ return unless options[:meta]
155
+
156
+ @template.content_tag(:p, options[:meta], class: "block text-black/60 text-sm mb-2")
157
+ end
158
+
159
+ private
160
+
161
+ def label_styles
162
+ "font-light inline-block mb-1 text-base leading-6"
163
+ end
164
+
165
+ def base_input_styles
166
+ "bg-white block w-full rounded-md border border-gray-500 focus:border-gray-700 p-2 text-gray-900 outline-0 focus:outline-0 ring-0 focus:ring-0 focus:ring-gray-700 ring-offset-0 focus:ring-offset-0 shadow-none focus:shadow-none"
167
+ end
168
+
169
+ def input_styles
170
+ base_input_styles
171
+ end
172
+
173
+ def input_styles_prefix
174
+ input_styles.concat(" prefix")
175
+ end
176
+
177
+ def select_styles
178
+ "col-start-1 row-start-1 w-full appearance-none rounded-md bg-white py-1.5 pl-3 pr-8 text-gray-900 text-base outline-0 outline-gray-700 focus:outline focus:-outline-offset-2 focus:outline-gray-700"
179
+ end
180
+
181
+ def select_svg
182
+ @template.content_tag(:svg,
183
+ class: "pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-gray-400", aria_hidden: true) do
184
+ @template.content_tag(:path, d: "M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z")
185
+ end
186
+ end
187
+
188
+ def button_styles
189
+ "inline-flex items-center rounded-md font-medium shadow-sm focus-visible:outline focus-visible:outline-0 focus-visible:outline-offset-none text-gray-700 border-2 border-gray-500 bg-transparent hover:bg-gray-100 transition-all gap-x-1.5 px-3 py-2 text-base gap-x-1.5 px-2.5 py-1.5 mt-2 "
190
+ end
191
+
192
+ def container_styles
193
+ "panda-core-field-container mb-4"
194
+ end
195
+
196
+ def textarea_styles
197
+ input_styles.concat(" min-h-32")
198
+ end
199
+
200
+ def submit_default_value
201
+ object.persisted? ? "Update #{object.class.name.demodulize}" : "Create #{object.class.name.demodulize}"
202
+ end
203
+
204
+ def wrap_field(method, options = {}, &block)
205
+ @template.content_tag(:div, class: "panda-core-field-container") do
206
+ label(method, class: "font-light inline-block mb-1 text-base leading-6") +
207
+ meta_text(options) +
208
+ @template.content_tag(:div, class: field_wrapper_styles, &block)
209
+ end
210
+ end
211
+
212
+ def field_wrapper_styles
213
+ "mt-1"
214
+ end
215
+
216
+ def error_message(attribute)
217
+ return unless object.respond_to?(:errors) && object.errors[attribute]&.any?
218
+
219
+ content_tag(:p, class: "mt-2 text-sm text-red-600") do
220
+ object.errors[attribute].join(", ")
221
+ end
222
+ end
223
+ end
224
+ end
225
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ module Admin
6
+ class ButtonComponent < ViewComponent::Base
7
+ attr_accessor :text, :action, :link, :icon, :size, :data
8
+
9
+ def initialize(text: "Button", action: nil, data: {}, link: "#", icon: nil, size: :regular, id: nil)
10
+ @text = text
11
+ @action = action
12
+ @data = data
13
+ @link = link
14
+ @icon = icon
15
+ @size = size
16
+ @id = id
17
+ end
18
+
19
+ def call
20
+ @icon = set_icon_from_action(@action) if @action && @icon.nil?
21
+ icon = content_tag(:i, "", class: "mr-2 fa-regular fa-#{@icon}") if @icon
22
+ @text = "#{icon} #{@text.titleize}".html_safe
23
+
24
+ classes = "inline-flex items-center rounded-md font-medium shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 "
25
+
26
+ case @size
27
+ when :small, :sm
28
+ classes += "gap-x-1.5 px-2.5 py-1.5 text-sm "
29
+ when :medium, :regular, :md
30
+ classes += "gap-x-1.5 px-3 py-2 text-base "
31
+ when :large, :lg
32
+ classes += "gap-x-2 px-3.5 py-2.5 text-lg "
33
+ end
34
+
35
+ classes += case @action
36
+ when :save, :create
37
+ "text-white bg-green-600 hover:bg-green-700"
38
+ when :save_inactive
39
+ "text-white bg-gray-400"
40
+ when :secondary
41
+ "text-gray-700 border-2 border-gray-700 bg-transparent hover:bg-gray-100 transition-all "
42
+ when :delete, :destroy, :danger
43
+ "text-red-600 border border-red-600 bg-red-100 hover:bg-red-200 hover:text-red-700 focus-visible:outline-red-300 "
44
+ else
45
+ "text-gray-700 border-2 border-gray-700 bg-transparent hover:bg-gray-100 transition-all "
46
+ end
47
+
48
+ content_tag :a, href: @link, class: classes, data: @data, id: @id do
49
+ @text
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ def set_icon_from_action(action)
56
+ case action
57
+ when :add, :new, :create
58
+ "plus"
59
+ when :save
60
+ "check"
61
+ when :edit, :update
62
+ "pencil"
63
+ when :delete, :destroy
64
+ "trash"
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,12 @@
1
+ <main class="overflow-auto flex-1 h-full min-h-full max-h-full">
2
+ <div class="overflow-auto px-2 pt-4 mx-auto sm:px-6 lg:px-6">
3
+ <%= heading %>
4
+ <%= tab_bar %>
5
+ <section class="flex-auto h-[calc(100vh-10rem)]">
6
+ <div class="flex-1 mt-4 w-full h-full">
7
+ <%= content %>
8
+ </div>
9
+ <%= slideover %>
10
+ </section>
11
+ </div>
12
+ </main>
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ module Admin
6
+ class ContainerComponent < ViewComponent::Base
7
+ renders_one :heading, "Panda::Core::Admin::HeadingComponent"
8
+ renders_one :tab_bar, "Panda::Core::Admin::TabBarComponent"
9
+ renders_one :slideover, "Panda::Core::Admin::SlideoverComponent"
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,31 @@
1
+ <div class="fixed top-2 right-2 z-[9999] p-2 space-y-4 w-full max-w-sm sm:items-end"
2
+ data-controller="alert"
3
+ <% if @temporary %> data-alert-dismiss-after-value="3000"<% end %>
4
+ data-transition-enter="ease-in-out duration-500"
5
+ data-transition-enter-from="translate-x-full opacity-0"
6
+ data-transition-enter-to="translate-x-0 opacity-100"
7
+ data-transition-leave="ease-in-out duration-500"
8
+ data-transition-leave-from="translate-x-0 opacity-100"
9
+ data-transition-leave-to="translate-x-full opacity-0">
10
+ <div class="overflow-hidden w-full max-w-sm bg-white rounded-lg ring-1 ring-black ring-opacity-5 shadow-lg">
11
+ <div class="p-4">
12
+ <div class="flex items-start">
13
+ <div class="flex-shrink-0">
14
+ <i class="fa-regular text-xl <%= icon_css %> <%= text_colour_css %>"></i>
15
+ </div>
16
+ <div class="flex-1 pt-0.5 ml-3 w-0">
17
+ <p class="mb-1 text-sm font-medium flash-message-title <%= text_colour_css %>"><%= kind.to_s.titleize %></p>
18
+ <p class="mt-1 mb-0 text-sm text-gray-500 flash-message-text"><%= message %></p>
19
+ </div>
20
+ <div class="flex flex-shrink-0 ml-4">
21
+ <button data-action="alert#close" type="button" class="inline-flex text-gray-400 bg-white rounded-md transition duration-150 ease-in-out hover:text-gray-500 focus:ring-2 focus:ring-offset-2 focus:outline-none focus:ring-sky-500">
22
+ <span class="sr-only">Close</span>
23
+ <svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
24
+ <path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" />
25
+ </svg>
26
+ </button>
27
+ </div>
28
+ </div>
29
+ </div>
30
+ </div>
31
+ </div>
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ module Admin
6
+ class FlashMessageComponent < ::ViewComponent::Base
7
+ attr_reader :kind, :message
8
+
9
+ def initialize(message:, kind:, temporary: true)
10
+ @kind = kind.to_sym
11
+ @message = message
12
+ @temporary = temporary
13
+ end
14
+
15
+ def text_colour_css
16
+ case kind
17
+ when :success
18
+ "text-green-600"
19
+ when :alert, :error
20
+ "text-red-600"
21
+ when :warning
22
+ "text-yellow-600"
23
+ when :info, :notice
24
+ "text-blue-600"
25
+ else
26
+ "text-gray-600"
27
+ end
28
+ end
29
+
30
+ def icon_css
31
+ case kind
32
+ when :success
33
+ "fa-circle-check"
34
+ when :alert
35
+ "fa-circle-xmark"
36
+ when :warning
37
+ "fa-triangle-exclamation"
38
+ when :info, :notice
39
+ "fa-circle-info"
40
+ else
41
+ "fa-circle-info"
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ module Admin
6
+ class HeadingComponent < ViewComponent::Base
7
+ renders_many :buttons, Panda::Core::Admin::ButtonComponent
8
+
9
+ attr_reader :text, :level, :icon, :additional_styles
10
+
11
+ def initialize(text:, level: 2, icon: "", additional_styles: "")
12
+ @text = text
13
+ @level = level
14
+ @icon = icon
15
+ @additional_styles = additional_styles
16
+ @additional_styles = @additional_styles.split(" ") if @additional_styles.is_a?(String)
17
+ end
18
+
19
+ def call
20
+ output = ""
21
+ output += content_tag(:div, @text, class: "grow")
22
+
23
+ if buttons?
24
+ output += content_tag(:span, class: "actions flex gap-x-2 -mt-1") do
25
+ safe_join(buttons, "")
26
+ end
27
+ end
28
+
29
+ output = output.html_safe
30
+ base_heading_styles = "flex pt-1 text-black mb-5 -mt-1"
31
+
32
+ case level
33
+ when 1
34
+ content_tag(:h1, output, class: [base_heading_styles, "text-2xl font-medium", @additional_styles])
35
+ when 2
36
+ content_tag(:h2, output, class: [base_heading_styles, "text-xl font-medium", @additional_styles])
37
+ when 3
38
+ content_tag(:h3, output, class: [base_heading_styles, "text-xl", "font-light", @additional_styles])
39
+ when :panel
40
+ content_tag(:h3, output, class: ["text-base font-medium p-4 text-white"])
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,7 @@
1
+ <div class="col-span-3 mt-5 rounded-lg shadow-md bg-gray-500 shadow-inherit/20">
2
+ <%= heading %>
3
+
4
+ <div class="p-4 text-black bg-white rounded-b-lg">
5
+ <%= content %>
6
+ </div>
7
+ </div>
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ module Admin
6
+ class PanelComponent < ViewComponent::Base
7
+ renders_one :heading, lambda { |text:, icon: "", level: :panel, additional_styles: ""|
8
+ Panda::Core::Admin::HeadingComponent.new(text: text, icon: icon, level: level, additional_styles: additional_styles)
9
+ }
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,9 @@
1
+ <% content_for :sidebar do %>
2
+ <aside class="hidden overflow-y-auto w-96 h-full bg-white lg:block">
3
+ <%= content %>
4
+ </aside>
5
+ <% end %>
6
+
7
+ <% content_for :sidebar_title do %>
8
+ <%= title %>
9
+ <% end %>
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ module Admin
6
+ class SlideoverComponent < ViewComponent::Base
7
+ attr_reader :title
8
+
9
+ def initialize(title: "Settings")
10
+ @title = title
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,29 @@
1
+ <div class="table overflow-x-auto mb-12 w-full rounded-lg border border-gray-500">
2
+ <div class="table-header-group">
3
+ <div class="table-row text-base font-medium text-white bg-gray-500">
4
+ <% columns.each_with_index do |column, i| %>
5
+ <div class="table-cell sticky top-0 z-10 p-4 <% if i.zero? %>rounded-tl-md<% elsif i == columns.size - 1 %>rounded-tr-md<% end %>"><%= column.label %></div>
6
+ <% end %>
7
+ </div>
8
+ </div>
9
+
10
+ <% if @rows.any? %>
11
+ <div class="table-row-group">
12
+ <% @rows.each do |row| %>
13
+ <div class="table-row relative bg-gray-500/5 hover:bg-gray-500/20" data-post-id="<%= row.id %>">
14
+ <% @columns.each do |column| %>
15
+ <div class="table-cell py-5 px-3 h-20 text-sm align-middle whitespace-nowrap border-b border-gray-500/20">
16
+ <%= view_context.capture(row, &column.cell) %>
17
+ </div>
18
+ <% end %>
19
+ </div>
20
+ <% end %>
21
+ </div>
22
+ </div>
23
+ <% else %>
24
+ </div>
25
+ <div class="text-center mx-12 block border border-dashed py-12 rounded-lg">
26
+ <h3 class="py-1 text-xl font-semibold text-gray-900">No <%= @term.pluralize %></h3>
27
+ <p class="py-1 text-base text-gray-500">Get started by creating a new <%= @term %>.</p>
28
+ </div>
29
+ <% end %>
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ module Admin
6
+ class TableComponent < ViewComponent::Base
7
+ attr_reader :columns
8
+
9
+ def initialize(term:, rows:)
10
+ @term = term
11
+ @rows = rows
12
+ @columns = []
13
+ end
14
+
15
+ def column(label, &)
16
+ @columns << Column.new(label, &)
17
+ end
18
+
19
+ private
20
+
21
+ # Ensures @columns gets populated [https://dev.to/rolandstuder/supercharged-table-component-built-with-viewcomponent-3j6i]
22
+ def before_render
23
+ content
24
+ end
25
+ end
26
+
27
+ class Column
28
+ attr_reader :label, :cell
29
+
30
+ def initialize(label, &block)
31
+ @label = label
32
+ @cell = block
33
+ end
34
+ end
35
+
36
+ class TagColumn < Column
37
+ attr_reader :label, :cell
38
+
39
+ def initialize(label, &block)
40
+ @label = label
41
+ @cell = Panda::Core::Admin::TagComponent.new(status: block)
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ module Admin
6
+ class TagComponent < ViewComponent::Base
7
+ attr_accessor :status, :text
8
+
9
+ def initialize(status: :active, text: nil)
10
+ @status = status.to_sym
11
+ @text = text || status.to_s.humanize
12
+ end
13
+
14
+ def call
15
+ classes = "inline-flex items-center py-1 px-2 text-xs font-medium rounded-md ring-1 ring-inset "
16
+
17
+ classes += case @status
18
+ when :active
19
+ "text-white ring-black/30 bg-green-600 border-0 "
20
+ when :draft
21
+ "text-black ring-black/30 bg-yellow-400 "
22
+ when :inactive, :hidden
23
+ "text-black ring-black/30 bg-black/5 bg-white "
24
+ else
25
+ "text-black bg-white "
26
+ end
27
+
28
+ content_tag :span, class: classes do
29
+ @text
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end