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.
- checksums.yaml +4 -4
- data/README.md +9 -16
- data/Rakefile +3 -0
- data/app/builders/panda/core/form_builder.rb +225 -0
- data/app/components/panda/core/admin/button_component.rb +70 -0
- data/app/components/panda/core/admin/container_component.html.erb +12 -0
- data/app/components/panda/core/admin/container_component.rb +13 -0
- data/app/components/panda/core/admin/flash_message_component.html.erb +31 -0
- data/app/components/panda/core/admin/flash_message_component.rb +47 -0
- data/app/components/panda/core/admin/heading_component.rb +46 -0
- data/app/components/panda/core/admin/panel_component.html.erb +7 -0
- data/app/components/panda/core/admin/panel_component.rb +13 -0
- data/app/components/panda/core/admin/slideover_component.html.erb +9 -0
- data/app/components/panda/core/admin/slideover_component.rb +15 -0
- data/app/components/panda/core/admin/table_component.html.erb +29 -0
- data/app/components/panda/core/admin/table_component.rb +46 -0
- data/app/components/panda/core/admin/tag_component.rb +35 -0
- data/app/constraints/panda/core/admin_constraint.rb +14 -0
- data/app/controllers/panda/core/admin/dashboard_controller.rb +22 -0
- data/app/controllers/panda/core/admin/my_profile_controller.rb +49 -0
- data/app/controllers/panda/core/admin/sessions_controller.rb +69 -0
- data/app/controllers/panda/core/admin_controller.rb +28 -0
- data/app/controllers/panda/core/application_controller.rb +59 -0
- data/app/helpers/panda/core/asset_helper.rb +32 -0
- data/app/javascript/panda/core/application.js +9 -0
- data/app/javascript/panda/core/controllers/index.js +20 -0
- data/app/javascript/panda/core/controllers/theme_form_controller.js +25 -0
- data/app/javascript/panda/core/tailwindcss-stimulus-components.js +3 -0
- data/app/models/panda/core/application_record.rb +9 -0
- data/app/models/panda/core/breadcrumb.rb +17 -0
- data/app/models/panda/core/current.rb +16 -0
- data/app/models/panda/core/user.rb +51 -0
- data/app/views/layouts/panda/core/admin.html.erb +59 -0
- data/app/views/panda/core/admin/dashboard/show.html.erb +27 -0
- data/app/views/panda/core/admin/my_profile/edit.html.erb +49 -0
- data/app/views/panda/core/admin/sessions/new.html.erb +38 -0
- data/app/views/panda/core/admin/shared/_breadcrumbs.html.erb +35 -0
- data/app/views/panda/core/admin/shared/_flash.html.erb +31 -0
- data/app/views/panda/core/admin/shared/_sidebar.html.erb +27 -0
- data/app/views/panda/core/admin/shared/_slideover.html.erb +33 -0
- data/config/routes.rb +22 -0
- data/db/migrate/20241210000003_add_current_theme_to_panda_core_users.rb +7 -0
- data/db/migrate/20250809000001_create_panda_core_users.rb +16 -0
- data/lib/generators/panda/core/dev_tools/USAGE +24 -0
- data/lib/generators/panda/core/dev_tools/templates/lefthook.yml +13 -0
- data/lib/generators/panda/core/dev_tools/templates/spec_support_panda_core_helpers.rb +18 -0
- data/lib/generators/panda/core/dev_tools_generator.rb +143 -0
- data/lib/panda/core/asset_loader.rb +221 -0
- data/lib/panda/core/authentication.rb +36 -0
- data/lib/panda/core/component_registry.rb +37 -0
- data/lib/panda/core/configuration.rb +31 -1
- data/lib/panda/core/engine.rb +43 -7
- data/lib/panda/core/notifications.rb +40 -0
- data/lib/panda/core/rake_tasks.rb +16 -0
- data/lib/panda/core/subscribers/authentication_subscriber.rb +61 -0
- data/lib/panda/core/testing/capybara_config.rb +70 -0
- data/lib/panda/core/testing/omniauth_helpers.rb +52 -0
- data/lib/panda/core/testing/rspec_config.rb +72 -0
- data/lib/panda/core/version.rb +1 -1
- data/lib/panda/core.rb +2 -8
- data/lib/tasks/assets.rake +423 -0
- data/lib/tasks/panda/core/migrations.rake +13 -0
- data/lib/tasks/panda_core.rake +52 -0
- metadata +320 -11
- data/db/migrate/20250121012333_logidze_install.rb +0 -577
- data/db/migrate/20250121012334_enable_hstore.rb +0 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ae9cc770f537f0190416f1dece9c54ff839015625d0145a1bd97b77cb667455a
|
4
|
+
data.tar.gz: e4f6286a4f7c6a20aa5c71ae5528f7203bcaf30ceddda2ea9c774bee6ffec66c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
-
|
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
|
-
|
123
|
-
|
124
|
-
git
|
125
|
-
gem
|
126
|
-
|
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
@@ -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,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,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
|