panda-core 0.6.0 → 0.7.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 +4 -4
- data/app/builders/panda/core/form_builder.rb +163 -11
- data/app/components/panda/core/admin/button_component.rb +27 -12
- data/app/components/panda/core/admin/container_component.rb +13 -1
- data/app/components/panda/core/admin/heading_component.rb +22 -14
- data/app/components/panda/core/admin/slideover_component.rb +52 -22
- data/app/controllers/panda/core/admin/my_profile_controller.rb +8 -1
- data/app/helpers/panda/core/asset_helper.rb +2 -0
- data/app/javascript/panda/core/controllers/image_cropper_controller.js +158 -0
- data/app/javascript/panda/core/controllers/index.js +6 -0
- data/app/javascript/panda/core/controllers/navigation_toggle_controller.js +60 -0
- data/app/models/panda/core/user.rb +13 -2
- data/app/services/panda/core/attach_avatar_service.rb +4 -0
- data/app/views/layouts/panda/core/admin.html.erb +39 -14
- data/app/views/panda/core/admin/dashboard/_default_content.html.erb +1 -1
- data/app/views/panda/core/admin/dashboard/show.html.erb +3 -3
- data/app/views/panda/core/admin/sessions/new.html.erb +1 -1
- data/app/views/panda/core/admin/shared/_breadcrumbs.html.erb +14 -24
- data/app/views/panda/core/admin/shared/_sidebar.html.erb +61 -11
- data/app/views/panda/core/admin/shared/_slideover.html.erb +1 -1
- data/config/importmap.rb +5 -0
- data/config/routes.rb +1 -1
- data/lib/panda/core/asset_loader.rb +5 -2
- data/lib/panda/core/engine.rb +26 -25
- data/lib/panda/core/oauth_providers.rb +3 -3
- data/lib/panda/core/version.rb +1 -1
- metadata +3 -69
- data/lib/generators/panda/core/authentication/templates/reek_spec.rb +0 -43
- data/lib/generators/panda/core/dev_tools/USAGE +0 -24
- data/lib/generators/panda/core/dev_tools/templates/lefthook.yml +0 -13
- data/lib/generators/panda/core/dev_tools/templates/spec_support_panda_core_helpers.rb +0 -18
- data/lib/generators/panda/core/dev_tools_generator.rb +0 -143
- data/lib/generators/panda/core/install_generator.rb +0 -41
- data/lib/generators/panda/core/templates/README +0 -25
- data/lib/generators/panda/core/templates/initializer.rb +0 -44
- data/lib/generators/panda/core/templates_generator.rb +0 -27
- data/lib/panda/core/testing/capybara_config.rb +0 -70
- data/lib/panda/core/testing/omniauth_helpers.rb +0 -52
- data/lib/panda/core/testing/rspec_config.rb +0 -72
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4aa145ee85e5b0c497754d7d218ceb3cc7c4de6065e3357f85e0676186df37f1
|
|
4
|
+
data.tar.gz: 9425c978a3208aca75045e4cf1e36408088829a73c19d10746a91cefc320db6f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ef76d583b1a36ecef8d13788338a847457bcd36a8efddf9582dcb1a7fc7ee484cb689f5dea6f9628d1fc46e2e7e2f513c84b6531116059496bb5e2e862d82f32
|
|
7
|
+
data.tar.gz: 982d52f5927bddf30b2c6ab1d3981e78e542fba07717628d73f4375c8819ebf29a151eaf08f0f89fea48d06830c52315d3ff9a996d19472696c061597d529bd4
|
|
@@ -11,6 +11,13 @@ module Panda
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def text_field(attribute, options = {})
|
|
14
|
+
# Add disabled/readonly styling
|
|
15
|
+
field_classes = if options[:readonly] || options[:disabled]
|
|
16
|
+
readonly_input_styles
|
|
17
|
+
else
|
|
18
|
+
input_styles
|
|
19
|
+
end
|
|
20
|
+
|
|
14
21
|
if options.dig(:data, :prefix)
|
|
15
22
|
content_tag :div, class: container_styles do
|
|
16
23
|
label(attribute) + meta_text(options) +
|
|
@@ -19,12 +26,12 @@ module Panda
|
|
|
19
26
|
class: "inline-flex items-center px-3 text-base border border-r-none rounded-s-md whitespace-nowrap break-keep") do
|
|
20
27
|
options.dig(:data, :prefix)
|
|
21
28
|
end +
|
|
22
|
-
super(attribute, options.reverse_merge(class: "#{
|
|
29
|
+
super(attribute, options.reverse_merge(class: "#{field_classes} input-prefix rounded-l-none border-l-none"))
|
|
23
30
|
end + error_message(attribute)
|
|
24
31
|
end
|
|
25
32
|
else
|
|
26
33
|
content_tag :div, class: container_styles do
|
|
27
|
-
label(attribute) + meta_text(options) + super(attribute, options.reverse_merge(class:
|
|
34
|
+
label(attribute) + meta_text(options) + super(attribute, options.reverse_merge(class: field_classes)) + error_message(attribute)
|
|
28
35
|
end
|
|
29
36
|
end
|
|
30
37
|
end
|
|
@@ -77,8 +84,121 @@ module Panda
|
|
|
77
84
|
end
|
|
78
85
|
|
|
79
86
|
def file_field(method, options = {})
|
|
80
|
-
|
|
81
|
-
|
|
87
|
+
# Check if cropper is requested
|
|
88
|
+
with_cropper = options.delete(:with_cropper)
|
|
89
|
+
|
|
90
|
+
# Check if simple mode is requested (no fancy upload UI)
|
|
91
|
+
simple_mode = options.delete(:simple)
|
|
92
|
+
|
|
93
|
+
if with_cropper
|
|
94
|
+
# Image upload with cropper
|
|
95
|
+
aspect_ratio = options.delete(:aspect_ratio) # e.g., 1.91 for OG images (1200x630)
|
|
96
|
+
min_width = options.delete(:min_width) || 0
|
|
97
|
+
min_height = options.delete(:min_height) || 0
|
|
98
|
+
accept_types = options.delete(:accept) || "image/*"
|
|
99
|
+
field_id = "#{object_name}_#{method}"
|
|
100
|
+
|
|
101
|
+
content_tag :div, class: container_styles do
|
|
102
|
+
label(method) +
|
|
103
|
+
meta_text(options) +
|
|
104
|
+
# Cropper stylesheet
|
|
105
|
+
@template.content_tag(:link, nil, rel: "stylesheet", href: "https://cdn.jsdelivr.net/npm/cropperjs@1.6.2/dist/cropper.min.css") +
|
|
106
|
+
# File input
|
|
107
|
+
content_tag(:div, class: "mt-2") do
|
|
108
|
+
super(method, options.reverse_merge(
|
|
109
|
+
id: field_id,
|
|
110
|
+
accept: accept_types,
|
|
111
|
+
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 #{input_styles}",
|
|
112
|
+
data: {
|
|
113
|
+
controller: "image-cropper",
|
|
114
|
+
image_cropper_target: "input",
|
|
115
|
+
action: "change->image-cropper#handleFileSelect",
|
|
116
|
+
image_cropper_aspect_ratio_value: aspect_ratio,
|
|
117
|
+
image_cropper_min_width_value: min_width,
|
|
118
|
+
image_cropper_min_height_value: min_height
|
|
119
|
+
}
|
|
120
|
+
))
|
|
121
|
+
end +
|
|
122
|
+
# Cropper container (hidden by default)
|
|
123
|
+
content_tag(:div, class: "hidden mt-4 bg-gray-100 dark:bg-gray-800 p-4 rounded-lg", data: {image_cropper_target: "cropperContainer"}) do
|
|
124
|
+
# Preview image
|
|
125
|
+
@template.image_tag("", alt: "Crop preview", data: {image_cropper_target: "preview"}, class: "max-w-full") +
|
|
126
|
+
# Cropper controls
|
|
127
|
+
content_tag(:div, class: "mt-4 flex gap-2 flex-wrap") do
|
|
128
|
+
@template.button_tag("Crop & Save", type: "button", class: "inline-flex items-center gap-x-1.5 rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-indigo-500", data: {action: "click->image-cropper#crop"}) +
|
|
129
|
+
@template.button_tag("Cancel", type: "button", class: "inline-flex items-center gap-x-1.5 rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-xs inset-ring inset-ring-gray-300 hover:bg-gray-50", data: {action: "click->image-cropper#cancel"}) +
|
|
130
|
+
@template.button_tag(type: "button", class: "inline-flex items-center gap-x-1.5 rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-xs inset-ring inset-ring-gray-300 hover:bg-gray-50", data: {action: "click->image-cropper#reset"}) do
|
|
131
|
+
@template.content_tag(:i, "", class: "fa-solid fa-rotate-left") +
|
|
132
|
+
@template.content_tag(:span, "Reset")
|
|
133
|
+
end +
|
|
134
|
+
@template.button_tag(type: "button", class: "inline-flex items-center gap-x-1.5 rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-xs inset-ring inset-ring-gray-300 hover:bg-gray-50", data: {action: "click->image-cropper#rotate", degrees: "90"}) do
|
|
135
|
+
@template.content_tag(:i, "", class: "fa-solid fa-rotate-right") +
|
|
136
|
+
@template.content_tag(:span, "Rotate")
|
|
137
|
+
end +
|
|
138
|
+
@template.button_tag(type: "button", class: "inline-flex items-center gap-x-1.5 rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-xs inset-ring inset-ring-gray-300 hover:bg-gray-50", data: {action: "click->image-cropper#flip", direction: "horizontal"}) do
|
|
139
|
+
@template.content_tag(:i, "", class: "fa-solid fa-arrows-left-right") +
|
|
140
|
+
@template.content_tag(:span, "Flip H")
|
|
141
|
+
end +
|
|
142
|
+
@template.button_tag(type: "button", class: "inline-flex items-center gap-x-1.5 rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-xs inset-ring inset-ring-gray-300 hover:bg-gray-50", data: {action: "click->image-cropper#zoom", ratio: "0.1"}) do
|
|
143
|
+
@template.content_tag(:i, "", class: "fa-solid fa-magnifying-glass-plus") +
|
|
144
|
+
@template.content_tag(:span, "Zoom In")
|
|
145
|
+
end +
|
|
146
|
+
@template.button_tag(type: "button", class: "inline-flex items-center gap-x-1.5 rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-xs inset-ring inset-ring-gray-300 hover:bg-gray-50", data: {action: "click->image-cropper#zoom", ratio: "-0.1"}) do
|
|
147
|
+
@template.content_tag(:i, "", class: "fa-solid fa-magnifying-glass-minus") +
|
|
148
|
+
@template.content_tag(:span, "Zoom Out")
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
elsif simple_mode
|
|
154
|
+
# Simple file input with basic styling
|
|
155
|
+
content_tag :div, class: container_styles do
|
|
156
|
+
label(method) +
|
|
157
|
+
meta_text(options) +
|
|
158
|
+
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)))
|
|
159
|
+
end
|
|
160
|
+
else
|
|
161
|
+
# Fancy drag-and-drop UI
|
|
162
|
+
accept_types = options.delete(:accept) || "image/*"
|
|
163
|
+
max_size = options.delete(:max_size) || "10MB"
|
|
164
|
+
file_types_display = options.delete(:file_types_display) || "PNG, JPG, GIF"
|
|
165
|
+
|
|
166
|
+
field_id = "#{object_name}_#{method}"
|
|
167
|
+
|
|
168
|
+
content_tag :div, class: "#{container_styles} col-span-full", data: {controller: "file-upload"} do
|
|
169
|
+
label(method) +
|
|
170
|
+
meta_text(options) +
|
|
171
|
+
content_tag(:div, class: "mt-2 flex justify-center rounded-lg border border-dashed border-gray-900/25 px-6 py-10 dark:border-white/25 transition-colors", data: {file_upload_target: "dropzone"}) do
|
|
172
|
+
content_tag(:div, class: "text-center") do
|
|
173
|
+
# Icon
|
|
174
|
+
@template.content_tag(:svg, viewBox: "0 0 24 24", fill: "currentColor", "data-slot": "icon", "aria-hidden": true, class: "mx-auto size-12 text-gray-300 dark:text-gray-600") do
|
|
175
|
+
@template.content_tag(:path, nil, d: "M1.5 6a2.25 2.25 0 0 1 2.25-2.25h16.5A2.25 2.25 0 0 1 22.5 6v12a2.25 2.25 0 0 1-2.25 2.25H3.75A2.25 2.25 0 0 1 1.5 18V6ZM3 16.06V18c0 .414.336.75.75.75h16.5A.75.75 0 0 0 21 18v-1.94l-2.69-2.689a1.5 1.5 0 0 0-2.12 0l-.88.879.97.97a.75.75 0 1 1-1.06 1.06l-5.16-5.159a1.5 1.5 0 0 0-2.12 0L3 16.061Zm10.125-7.81a1.125 1.125 0 1 1 2.25 0 1.125 1.125 0 0 1-2.25 0Z", "clip-rule": "evenodd", "fill-rule": "evenodd")
|
|
176
|
+
end +
|
|
177
|
+
# Upload area
|
|
178
|
+
content_tag(:div, class: "mt-4 flex items-baseline justify-center text-sm leading-6 text-gray-600 dark:text-gray-400") do
|
|
179
|
+
content_tag(:label, for: field_id, class: "relative cursor-pointer rounded-md bg-transparent font-semibold text-indigo-600 focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-indigo-600 hover:text-indigo-500 dark:text-indigo-400 dark:focus-within:outline-indigo-500 dark:hover:text-indigo-300") do
|
|
180
|
+
content_tag(:span, "Upload a file") +
|
|
181
|
+
super(method, options.reverse_merge(
|
|
182
|
+
id: field_id,
|
|
183
|
+
accept: accept_types,
|
|
184
|
+
class: "sr-only",
|
|
185
|
+
data: {
|
|
186
|
+
file_upload_target: "input",
|
|
187
|
+
action: "change->file-upload#handleFileSelect"
|
|
188
|
+
}
|
|
189
|
+
))
|
|
190
|
+
end +
|
|
191
|
+
content_tag(:span, "or drag and drop", class: "pl-1")
|
|
192
|
+
end +
|
|
193
|
+
# File type info
|
|
194
|
+
content_tag(:p, "#{file_types_display} up to #{max_size}", class: "text-xs/5 text-gray-600 dark:text-gray-400")
|
|
195
|
+
end
|
|
196
|
+
end +
|
|
197
|
+
# File info display (hidden by default)
|
|
198
|
+
content_tag(:div, "", class: "hidden mt-3", data: {file_upload_target: "fileInfo"}) +
|
|
199
|
+
# Preview display (hidden by default)
|
|
200
|
+
content_tag(:div, "", class: "hidden mt-3", data: {file_upload_target: "preview"})
|
|
201
|
+
end
|
|
82
202
|
end
|
|
83
203
|
end
|
|
84
204
|
|
|
@@ -117,11 +237,11 @@ module Panda
|
|
|
117
237
|
def submit(value = nil, options = {})
|
|
118
238
|
value ||= submit_default_value
|
|
119
239
|
|
|
120
|
-
# Use the
|
|
240
|
+
# Use the primary mid color for save/create actions
|
|
121
241
|
action = object.persisted? ? :save : :create
|
|
122
242
|
button_classes = case action
|
|
123
243
|
when :save, :create
|
|
124
|
-
"text-white bg-
|
|
244
|
+
"text-white bg-mid hover:bg-mid/80"
|
|
125
245
|
when :save_inactive
|
|
126
246
|
"text-white bg-gray-400"
|
|
127
247
|
when :secondary
|
|
@@ -150,32 +270,64 @@ module Panda
|
|
|
150
270
|
end
|
|
151
271
|
end
|
|
152
272
|
|
|
273
|
+
def radio_button_group(method, choices, options = {})
|
|
274
|
+
current_value = object.send(method)
|
|
275
|
+
|
|
276
|
+
content_tag :div, class: container_styles do
|
|
277
|
+
label(method) +
|
|
278
|
+
meta_text(options) +
|
|
279
|
+
content_tag(:div, class: "mt-2 space-y-2") do
|
|
280
|
+
choices.map do |choice|
|
|
281
|
+
choice_value = choice.is_a?(Array) ? choice.last : choice
|
|
282
|
+
choice_label = choice.is_a?(Array) ? choice.first : choice.to_s.humanize
|
|
283
|
+
choice_id = "#{object_name}_#{method}_#{choice_value}"
|
|
284
|
+
is_checked = (current_value.to_s == choice_value.to_s)
|
|
285
|
+
|
|
286
|
+
content_tag(:label, class: "flex items-center gap-x-3 rounded-lg border border-gray-300 px-3 py-3 text-sm/6 font-medium cursor-pointer hover:bg-gray-50 dark:border-white/10 dark:hover:bg-white/5") do
|
|
287
|
+
radio_button(method, choice_value, {id: choice_id, checked: is_checked, class: "size-4 border-gray-300 text-indigo-600 focus:ring-indigo-600 dark:border-white/10 dark:bg-white/5"}) +
|
|
288
|
+
content_tag(:span, choice_label, class: "text-gray-900 dark:text-white")
|
|
289
|
+
end
|
|
290
|
+
end.join.html_safe
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
153
295
|
def meta_text(options)
|
|
154
296
|
return unless options[:meta]
|
|
155
297
|
|
|
156
298
|
@template.content_tag(:p, options[:meta], class: "block text-black/60 text-sm mb-2")
|
|
157
299
|
end
|
|
158
300
|
|
|
301
|
+
def section_heading(text, options = {})
|
|
302
|
+
@template.content_tag(:div, class: "-mx-4 sm:-mx-6 px-4 sm:px-6 py-4 bg-gray-200 dark:bg-gray-700 mb-6") do
|
|
303
|
+
@template.content_tag(:h3, text, class: "text-base font-semibold text-gray-900 dark:text-white")
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
159
307
|
private
|
|
160
308
|
|
|
161
309
|
def label_styles
|
|
162
|
-
"
|
|
310
|
+
"block text-sm/6 font-medium text-gray-900 dark:text-gray-100"
|
|
163
311
|
end
|
|
164
312
|
|
|
165
313
|
def base_input_styles
|
|
166
|
-
"
|
|
314
|
+
"block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-indigo-600 sm:text-sm/6 dark:bg-white/5 dark:text-white dark:outline-white/10 dark:placeholder:text-gray-500 dark:focus:outline-indigo-500"
|
|
167
315
|
end
|
|
168
316
|
|
|
169
317
|
def input_styles
|
|
170
318
|
base_input_styles
|
|
171
319
|
end
|
|
172
320
|
|
|
321
|
+
def readonly_input_styles
|
|
322
|
+
"block w-full rounded-md bg-gray-50 px-3 py-1.5 text-base text-gray-500 outline-1 -outline-offset-1 outline-gray-200 cursor-not-allowed sm:text-sm/6 dark:bg-white/10 dark:text-gray-500 dark:outline-white/5"
|
|
323
|
+
end
|
|
324
|
+
|
|
173
325
|
def input_styles_prefix
|
|
174
|
-
input_styles
|
|
326
|
+
"#{input_styles} prefix"
|
|
175
327
|
end
|
|
176
328
|
|
|
177
329
|
def select_styles
|
|
178
|
-
"
|
|
330
|
+
"block w-full rounded-md bg-white px-3 py-1.5 pr-8 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 appearance-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-indigo-600 sm:text-sm/6 dark:bg-white/5 dark:text-white dark:outline-white/10 dark:focus:outline-indigo-500"
|
|
179
331
|
end
|
|
180
332
|
|
|
181
333
|
def select_svg
|
|
@@ -194,7 +346,7 @@ module Panda
|
|
|
194
346
|
end
|
|
195
347
|
|
|
196
348
|
def textarea_styles
|
|
197
|
-
input_styles
|
|
349
|
+
"#{input_styles} min-h-32"
|
|
198
350
|
end
|
|
199
351
|
|
|
200
352
|
def submit_default_value
|
|
@@ -6,37 +6,52 @@ module Panda
|
|
|
6
6
|
class ButtonComponent < Panda::Core::Base
|
|
7
7
|
prop :text, String, default: "Button"
|
|
8
8
|
prop :action, _Nilable(Symbol), default: -> {}
|
|
9
|
-
prop :href, String, default: "#"
|
|
9
|
+
prop :href, _Nilable(String), default: -> { "#" }
|
|
10
10
|
prop :icon, _Nilable(String), default: -> {}
|
|
11
11
|
prop :size, Symbol, default: :regular
|
|
12
12
|
prop :id, _Nilable(String), default: -> {}
|
|
13
|
+
prop :as_button, _Boolean, default: -> { false }
|
|
13
14
|
|
|
14
15
|
def view_template
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
if @as_button
|
|
17
|
+
button(**@attrs) do
|
|
18
|
+
render_content
|
|
19
|
+
end
|
|
20
|
+
else
|
|
21
|
+
a(**@attrs) do
|
|
22
|
+
render_content
|
|
19
23
|
end
|
|
20
|
-
plain @text.titleize
|
|
21
24
|
end
|
|
22
25
|
end
|
|
23
26
|
|
|
24
27
|
def default_attrs
|
|
25
|
-
{
|
|
26
|
-
href: @href,
|
|
28
|
+
base = {
|
|
27
29
|
class: button_classes,
|
|
28
30
|
id: @id
|
|
29
31
|
}
|
|
32
|
+
|
|
33
|
+
if @as_button
|
|
34
|
+
base.merge(type: "button")
|
|
35
|
+
else
|
|
36
|
+
base.merge(href: @href)
|
|
37
|
+
end
|
|
30
38
|
end
|
|
31
39
|
|
|
32
40
|
private
|
|
33
41
|
|
|
42
|
+
def render_content
|
|
43
|
+
if computed_icon
|
|
44
|
+
i(class: "fa-solid fa-#{computed_icon}")
|
|
45
|
+
end
|
|
46
|
+
plain @text.titleize
|
|
47
|
+
end
|
|
48
|
+
|
|
34
49
|
def computed_icon
|
|
35
50
|
@computed_icon ||= @icon || icon_from_action(@action)
|
|
36
51
|
end
|
|
37
52
|
|
|
38
53
|
def button_classes
|
|
39
|
-
base = "inline-flex items-center rounded-md font-medium shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 "
|
|
54
|
+
base = "inline-flex items-center rounded-md font-medium shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 cursor-pointer "
|
|
40
55
|
base + size_classes + action_classes
|
|
41
56
|
end
|
|
42
57
|
|
|
@@ -56,15 +71,15 @@ module Panda
|
|
|
56
71
|
def action_classes
|
|
57
72
|
case @action
|
|
58
73
|
when :save, :create
|
|
59
|
-
"text-white bg-
|
|
74
|
+
"text-white bg-mid hover:bg-mid/80"
|
|
60
75
|
when :save_inactive
|
|
61
76
|
"text-white bg-gray-400"
|
|
62
77
|
when :secondary
|
|
63
|
-
"text-gray-700 border-2 border-gray-
|
|
78
|
+
"text-gray-700 border-2 border-gray-500 bg-white hover:bg-gray-100 active:bg-gray-200 transition-colors"
|
|
64
79
|
when :delete, :destroy, :danger
|
|
65
80
|
"text-red-600 border border-red-600 bg-red-100 hover:bg-red-200 hover:text-red-700 focus-visible:outline-red-300"
|
|
66
81
|
else
|
|
67
|
-
"text-gray-700 border-2 border-gray-
|
|
82
|
+
"text-gray-700 border-2 border-gray-500 bg-white hover:bg-gray-100 active:bg-gray-200 transition-colors"
|
|
68
83
|
end
|
|
69
84
|
end
|
|
70
85
|
|
|
@@ -26,10 +26,17 @@ module Panda
|
|
|
26
26
|
view_context.capture(&@slideover_block)
|
|
27
27
|
end
|
|
28
28
|
view_context.content_for(:sidebar_title, @slideover_title)
|
|
29
|
+
|
|
30
|
+
# Set footer content if present
|
|
31
|
+
if @footer_block
|
|
32
|
+
view_context.content_for(:sidebar_footer) do
|
|
33
|
+
view_context.capture(&@footer_block)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
29
36
|
end
|
|
30
37
|
|
|
31
38
|
main(class: "overflow-auto flex-1 h-full min-h-full max-h-full") do
|
|
32
|
-
div(class: "overflow-auto px-2 pt-
|
|
39
|
+
div(class: "overflow-auto px-2 pt-2 mx-auto sm:px-6 lg:px-6") do
|
|
33
40
|
@heading_content&.call
|
|
34
41
|
@tab_bar_content&.call
|
|
35
42
|
|
|
@@ -68,8 +75,13 @@ module Panda
|
|
|
68
75
|
@slideover_block = block # Save the block for content_for
|
|
69
76
|
end
|
|
70
77
|
|
|
78
|
+
def footer(&block)
|
|
79
|
+
@footer_block = block
|
|
80
|
+
end
|
|
81
|
+
|
|
71
82
|
# Alias for ViewComponent-style API compatibility
|
|
72
83
|
alias_method :with_slideover, :slideover
|
|
84
|
+
alias_method :with_footer, :footer
|
|
73
85
|
|
|
74
86
|
private
|
|
75
87
|
|
|
@@ -7,23 +7,30 @@ module Panda
|
|
|
7
7
|
prop :text, String
|
|
8
8
|
prop :level, _Nilable(_Union(Integer, Symbol)), default: -> { 2 }
|
|
9
9
|
prop :icon, String, default: ""
|
|
10
|
+
prop :meta, _Nilable(String), default: -> {}
|
|
10
11
|
prop :additional_styles, _Nilable(_Union(String, Array)), default: -> { "" }
|
|
11
12
|
|
|
12
13
|
def view_template(&block)
|
|
13
14
|
# Capture any buttons defined via block
|
|
14
15
|
instance_eval(&block) if block_given?
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
17
|
+
div(class: "heading-wrapper") do
|
|
18
|
+
case @level
|
|
19
|
+
when 1
|
|
20
|
+
h1(class: heading_classes(@meta.present?)) { render_content }
|
|
21
|
+
when 2
|
|
22
|
+
h2(class: heading_classes(@meta.present?)) { render_content }
|
|
23
|
+
when 3
|
|
24
|
+
h3(class: heading_classes(@meta.present?)) { render_content }
|
|
25
|
+
when :panel
|
|
26
|
+
h3(class: panel_heading_classes) { @text }
|
|
27
|
+
else
|
|
28
|
+
h2(class: heading_classes(@meta.present?)) { render_content }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
if @meta
|
|
32
|
+
p(class: "text-sm text-black/60 -mt-1 mb-5") { raw(@meta) }
|
|
33
|
+
end
|
|
27
34
|
end
|
|
28
35
|
end
|
|
29
36
|
|
|
@@ -40,13 +47,14 @@ module Panda
|
|
|
40
47
|
span { @text }
|
|
41
48
|
end
|
|
42
49
|
|
|
43
|
-
span(class: "actions flex gap-x-2
|
|
50
|
+
span(class: "actions flex gap-x-2 mt-1 min-h-[2.5rem]") do
|
|
44
51
|
@buttons&.each { |btn| render(btn) }
|
|
45
52
|
end
|
|
46
53
|
end
|
|
47
54
|
|
|
48
|
-
def heading_classes
|
|
49
|
-
|
|
55
|
+
def heading_classes(has_meta = false)
|
|
56
|
+
margin_bottom = has_meta ? "mb-0.5" : "mb-5"
|
|
57
|
+
base = "flex text-black #{margin_bottom} -mt-2"
|
|
50
58
|
styles = case @level
|
|
51
59
|
when 1
|
|
52
60
|
"text-2xl font-medium"
|
|
@@ -21,42 +21,72 @@ module Panda
|
|
|
21
21
|
**default_attrs,
|
|
22
22
|
data: {
|
|
23
23
|
toggle_target: "toggleable",
|
|
24
|
-
transition_enter: "transform transition ease-in-out duration-500",
|
|
24
|
+
transition_enter: "transform transition ease-in-out duration-500 sm:duration-700",
|
|
25
25
|
transition_enter_from: "translate-x-full",
|
|
26
26
|
transition_enter_to: "translate-x-0",
|
|
27
|
-
transition_leave: "transform transition ease-in-out duration-500",
|
|
27
|
+
transition_leave: "transform transition ease-in-out duration-500 sm:duration-700",
|
|
28
28
|
transition_leave_from: "translate-x-0",
|
|
29
29
|
transition_leave_to: "translate-x-full"
|
|
30
30
|
}
|
|
31
31
|
) do
|
|
32
|
-
#
|
|
33
|
-
div(class: "
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
32
|
+
# Main container
|
|
33
|
+
div(class: "relative flex h-full flex-col bg-white shadow-xl dark:bg-gray-800") do
|
|
34
|
+
# Header with title and close button
|
|
35
|
+
div(class: "bg-gradient-admin px-4 py-6 sm:px-6") do
|
|
36
|
+
div(class: "flex items-center justify-between") do
|
|
37
|
+
h2(class: "text-base font-semibold text-white", id: "slideover-title") do
|
|
38
|
+
plain @title
|
|
39
|
+
end
|
|
40
|
+
div(class: "ml-3 flex h-7 items-center") do
|
|
41
|
+
button(
|
|
42
|
+
type: "button",
|
|
43
|
+
data: {action: "click->toggle#toggle touch->toggle#toggle"},
|
|
44
|
+
class: "relative rounded-md text-white/80 hover:text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white"
|
|
45
|
+
) do
|
|
46
|
+
span(class: "absolute -inset-2.5")
|
|
47
|
+
span(class: "sr-only") { "Close panel" }
|
|
48
|
+
# SVG close icon
|
|
49
|
+
svg(viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", stroke_width: "1.5", aria_hidden: "true", class: "size-6") do
|
|
50
|
+
path(d: "M6 18 18 6M6 6l12 12", stroke_linecap: "round", stroke_linejoin: "round")
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
38
54
|
end
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Scrollable content area
|
|
58
|
+
div(class: "flex-1 overflow-y-auto") do
|
|
59
|
+
if @content_html
|
|
60
|
+
raw(@content_html)
|
|
61
|
+
elsif @content_block
|
|
62
|
+
instance_eval(@content_block)
|
|
45
63
|
end
|
|
46
64
|
end
|
|
47
|
-
end
|
|
48
65
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
66
|
+
# Sticky footer (if footer content exists)
|
|
67
|
+
if @footer_html || @footer_block
|
|
68
|
+
div(class: "flex shrink-0 justify-end gap-x-3 border-t border-gray-200 px-4 py-4 dark:border-white/10") do
|
|
69
|
+
if @footer_html
|
|
70
|
+
raw(@footer_html)
|
|
71
|
+
elsif @footer_block
|
|
72
|
+
instance_eval(&@footer_block)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
55
75
|
end
|
|
56
76
|
end
|
|
57
77
|
end
|
|
58
78
|
end
|
|
59
79
|
|
|
80
|
+
def footer(&block)
|
|
81
|
+
if defined?(view_context) && view_context
|
|
82
|
+
@footer_html = view_context.capture(&block)
|
|
83
|
+
else
|
|
84
|
+
@footer_block = block
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
alias_method :with_footer, :footer
|
|
89
|
+
|
|
60
90
|
private
|
|
61
91
|
|
|
62
92
|
def default_attrs
|
|
@@ -67,7 +97,7 @@ module Panda
|
|
|
67
97
|
end
|
|
68
98
|
|
|
69
99
|
def slideover_classes
|
|
70
|
-
base = "
|
|
100
|
+
base = "ml-auto block size-full max-w-md transform absolute right-0 h-full z-50"
|
|
71
101
|
visibility = @open ? "" : "hidden"
|
|
72
102
|
[base, visibility].compact.join(" ")
|
|
73
103
|
end
|
|
@@ -4,7 +4,14 @@ module Panda
|
|
|
4
4
|
module Core
|
|
5
5
|
module Admin
|
|
6
6
|
class MyProfileController < BaseController
|
|
7
|
-
before_action :set_initial_breadcrumb, only: %i[edit update]
|
|
7
|
+
before_action :set_initial_breadcrumb, only: %i[show edit update]
|
|
8
|
+
|
|
9
|
+
# Redirects to the edit form
|
|
10
|
+
# @type GET
|
|
11
|
+
# @return void
|
|
12
|
+
def show
|
|
13
|
+
redirect_to edit_admin_my_profile_path
|
|
14
|
+
end
|
|
8
15
|
|
|
9
16
|
# Shows the edit form for the current user's profile
|
|
10
17
|
# @type GET
|
|
@@ -33,6 +33,8 @@ module Panda
|
|
|
33
33
|
"@rails/actioncable/src": "/panda/core/vendor/@rails--actioncable--src.js",
|
|
34
34
|
"tailwindcss-stimulus-components": "/panda/core/tailwindcss-stimulus-components.js",
|
|
35
35
|
"@fortawesome/fontawesome-free": "https://ga.jspm.io/npm:@fortawesome/fontawesome-free@7.1.0/js/all.js",
|
|
36
|
+
"@tailwindplus/elements": "https://esm.sh/@tailwindplus/elements@1",
|
|
37
|
+
"cropperjs": "https://esm.sh/cropperjs@1.6.2",
|
|
36
38
|
"panda/core/application": "/panda/core/application.js",
|
|
37
39
|
"panda/core/controllers/toggle_controller": "/panda/core/controllers/toggle_controller.js",
|
|
38
40
|
"panda/core/controllers/theme_form_controller": "/panda/core/controllers/theme_form_controller.js"
|