panda-core 0.2.3 → 0.4.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 +185 -0
- data/app/assets/tailwind/application.css +279 -0
- data/app/assets/tailwind/tailwind.config.js +21 -0
- data/app/components/panda/core/UI/badge.rb +107 -0
- data/app/components/panda/core/UI/button.rb +89 -0
- data/app/components/panda/core/UI/card.rb +88 -0
- data/app/components/panda/core/admin/button_component.rb +46 -28
- data/app/components/panda/core/admin/container_component.rb +52 -4
- data/app/components/panda/core/admin/flash_message_component.rb +74 -9
- data/app/components/panda/core/admin/form_error_component.rb +48 -0
- data/app/components/panda/core/admin/form_input_component.rb +50 -0
- data/app/components/panda/core/admin/form_select_component.rb +68 -0
- data/app/components/panda/core/admin/heading_component.rb +52 -24
- data/app/components/panda/core/admin/panel_component.rb +33 -4
- data/app/components/panda/core/admin/slideover_component.rb +8 -4
- data/app/components/panda/core/admin/statistics_component.rb +19 -0
- data/app/components/panda/core/admin/tab_bar_component.rb +101 -0
- data/app/components/panda/core/admin/table_component.rb +90 -9
- data/app/components/panda/core/admin/tag_component.rb +21 -16
- data/app/components/panda/core/admin/user_activity_component.rb +43 -0
- data/app/components/panda/core/admin/user_display_component.rb +78 -0
- data/app/components/panda/core/base.rb +122 -0
- data/app/controllers/panda/core/admin/base_controller.rb +68 -0
- data/app/controllers/panda/core/admin/dashboard_controller.rb +7 -6
- data/app/controllers/panda/core/admin/my_profile_controller.rb +3 -3
- data/app/controllers/panda/core/admin/sessions_controller.rb +26 -5
- data/app/helpers/panda/core/sessions_helper.rb +21 -0
- data/app/javascript/panda/core/application.js +1 -0
- data/app/javascript/panda/core/vendor/@hotwired--stimulus.js +4 -0
- data/app/javascript/panda/core/vendor/@hotwired--turbo.js +160 -0
- data/app/javascript/panda/core/vendor/@rails--actioncable--src.js +4 -0
- data/app/models/panda/core/user.rb +17 -13
- data/app/views/layouts/panda/core/admin.html.erb +40 -57
- data/app/views/layouts/panda/core/admin_simple.html.erb +5 -0
- data/app/views/panda/core/admin/dashboard/_default_content.html.erb +73 -0
- data/app/views/panda/core/admin/dashboard/show.html.erb +4 -10
- data/app/views/panda/core/admin/my_profile/edit.html.erb +13 -27
- data/app/views/panda/core/admin/sessions/new.html.erb +13 -12
- data/app/views/panda/core/admin/shared/_breadcrumbs.html.erb +27 -34
- data/app/views/panda/core/admin/shared/_flash.html.erb +4 -30
- data/app/views/panda/core/admin/shared/_sidebar.html.erb +36 -20
- data/app/views/panda/core/shared/_footer.html.erb +2 -0
- data/app/views/panda/core/shared/_header.html.erb +19 -0
- data/config/importmap.rb +15 -0
- data/config/initializers/panda_core.rb +37 -1
- data/config/routes.rb +7 -7
- data/db/migrate/20250810120000_add_current_theme_to_panda_core_users.rb +7 -0
- data/lib/generators/panda/core/install_generator.rb +3 -9
- data/lib/generators/panda/core/templates/README +25 -0
- data/lib/generators/panda/core/templates/initializer.rb +28 -0
- data/lib/panda/core/asset_loader.rb +23 -8
- data/lib/panda/core/configuration.rb +41 -9
- data/lib/panda/core/debug.rb +47 -0
- data/lib/panda/core/engine.rb +82 -8
- data/lib/panda/core/version.rb +1 -1
- data/lib/panda/core.rb +1 -0
- data/lib/tasks/assets.rake +58 -392
- data/lib/tasks/panda_core_tasks.rake +16 -0
- metadata +102 -14
- data/app/components/panda/core/admin/container_component.html.erb +0 -12
- data/app/components/panda/core/admin/flash_message_component.html.erb +0 -31
- data/app/components/panda/core/admin/panel_component.html.erb +0 -7
- data/app/components/panda/core/admin/slideover_component.html.erb +0 -9
- data/app/components/panda/core/admin/table_component.html.erb +0 -29
- data/app/controllers/panda/core/admin_controller.rb +0 -28
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// @rails/actioncable/src@7.2.101 downloaded from https://ga.jspm.io/npm:@rails/actioncable@7.2.101/src/index.js
|
|
2
|
+
|
|
3
|
+
var t={logger:typeof console!=="undefined"?console:void 0,WebSocket:typeof WebSocket!=="undefined"?WebSocket:void 0};var e={log(...e){if(this.enabled){e.push(Date.now());t.logger.log("[ActionCable]",...e)}}};const now=()=>(new Date).getTime();const secondsSince=t=>(now()-t)/1e3;class ConnectionMonitor{constructor(t){this.visibilityDidChange=this.visibilityDidChange.bind(this);this.connection=t;this.reconnectAttempts=0}start(){if(!this.isRunning()){this.startedAt=now();delete this.stoppedAt;this.startPolling();addEventListener("visibilitychange",this.visibilityDidChange);e.log(`ConnectionMonitor started. stale threshold = ${this.constructor.staleThreshold} s`)}}stop(){if(this.isRunning()){this.stoppedAt=now();this.stopPolling();removeEventListener("visibilitychange",this.visibilityDidChange);e.log("ConnectionMonitor stopped")}}isRunning(){return this.startedAt&&!this.stoppedAt}recordMessage(){this.pingedAt=now()}recordConnect(){this.reconnectAttempts=0;delete this.disconnectedAt;e.log("ConnectionMonitor recorded connect")}recordDisconnect(){this.disconnectedAt=now();e.log("ConnectionMonitor recorded disconnect")}startPolling(){this.stopPolling();this.poll()}stopPolling(){clearTimeout(this.pollTimeout)}poll(){this.pollTimeout=setTimeout((()=>{this.reconnectIfStale();this.poll()}),this.getPollInterval())}getPollInterval(){const{staleThreshold:t,reconnectionBackoffRate:e}=this.constructor;const n=Math.pow(1+e,Math.min(this.reconnectAttempts,10));const i=this.reconnectAttempts===0?1:e;const o=i*Math.random();return t*1e3*n*(1+o)}reconnectIfStale(){if(this.connectionIsStale()){e.log(`ConnectionMonitor detected stale connection. reconnectAttempts = ${this.reconnectAttempts}, time stale = ${secondsSince(this.refreshedAt)} s, stale threshold = ${this.constructor.staleThreshold} s`);this.reconnectAttempts++;if(this.disconnectedRecently())e.log(`ConnectionMonitor skipping reopening recent disconnect. time disconnected = ${secondsSince(this.disconnectedAt)} s`);else{e.log("ConnectionMonitor reopening");this.connection.reopen()}}}get refreshedAt(){return this.pingedAt?this.pingedAt:this.startedAt}connectionIsStale(){return secondsSince(this.refreshedAt)>this.constructor.staleThreshold}disconnectedRecently(){return this.disconnectedAt&&secondsSince(this.disconnectedAt)<this.constructor.staleThreshold}visibilityDidChange(){document.visibilityState==="visible"&&setTimeout((()=>{if(this.connectionIsStale()||!this.connection.isOpen()){e.log(`ConnectionMonitor reopening stale connection on visibilitychange. visibilityState = ${document.visibilityState}`);this.connection.reopen()}}),200)}}ConnectionMonitor.staleThreshold=6;ConnectionMonitor.reconnectionBackoffRate=.15;var n={message_types:{welcome:"welcome",disconnect:"disconnect",ping:"ping",confirmation:"confirm_subscription",rejection:"reject_subscription"},disconnect_reasons:{unauthorized:"unauthorized",invalid_request:"invalid_request",server_restart:"server_restart",remote:"remote"},default_mount_path:"/cable",protocols:["actioncable-v1-json","actioncable-unsupported"]};const{message_types:i,protocols:o}=n;const s=o.slice(0,o.length-1);const r=[].indexOf;class Connection{constructor(t){this.open=this.open.bind(this);this.consumer=t;this.subscriptions=this.consumer.subscriptions;this.monitor=new ConnectionMonitor(this);this.disconnected=true}send(t){if(this.isOpen()){this.webSocket.send(JSON.stringify(t));return true}return false}open(){if(this.isActive()){e.log(`Attempted to open WebSocket, but existing socket is ${this.getState()}`);return false}{const n=[...o,...this.consumer.subprotocols||[]];e.log(`Opening WebSocket, current state is ${this.getState()}, subprotocols: ${n}`);this.webSocket&&this.uninstallEventHandlers();this.webSocket=new t.WebSocket(this.consumer.url,n);this.installEventHandlers();this.monitor.start();return true}}close({allowReconnect:t}={allowReconnect:true}){t||this.monitor.stop();if(this.isOpen())return this.webSocket.close()}reopen(){e.log(`Reopening WebSocket, current state is ${this.getState()}`);if(!this.isActive())return this.open();try{return this.close()}catch(t){e.log("Failed to reopen WebSocket",t)}finally{e.log(`Reopening WebSocket in ${this.constructor.reopenDelay}ms`);setTimeout(this.open,this.constructor.reopenDelay)}}getProtocol(){if(this.webSocket)return this.webSocket.protocol}isOpen(){return this.isState("open")}isActive(){return this.isState("open","connecting")}triedToReconnect(){return this.monitor.reconnectAttempts>0}isProtocolSupported(){return r.call(s,this.getProtocol())>=0}isState(...t){return r.call(t,this.getState())>=0}getState(){if(this.webSocket)for(let e in t.WebSocket)if(t.WebSocket[e]===this.webSocket.readyState)return e.toLowerCase();return null}installEventHandlers(){for(let t in this.events){const e=this.events[t].bind(this);this.webSocket[`on${t}`]=e}}uninstallEventHandlers(){for(let t in this.events)this.webSocket[`on${t}`]=function(){}}}Connection.reopenDelay=500;Connection.prototype.events={message(t){if(!this.isProtocolSupported())return;const{identifier:n,message:o,reason:s,reconnect:r,type:c}=JSON.parse(t.data);this.monitor.recordMessage();switch(c){case i.welcome:this.triedToReconnect()&&(this.reconnectAttempted=true);this.monitor.recordConnect();return this.subscriptions.reload();case i.disconnect:e.log(`Disconnecting. Reason: ${s}`);return this.close({allowReconnect:r});case i.ping:return null;case i.confirmation:this.subscriptions.confirmSubscription(n);if(this.reconnectAttempted){this.reconnectAttempted=false;return this.subscriptions.notify(n,"connected",{reconnected:true})}return this.subscriptions.notify(n,"connected",{reconnected:false});case i.rejection:return this.subscriptions.reject(n);default:return this.subscriptions.notify(n,"received",o)}},open(){e.log(`WebSocket onopen event, using '${this.getProtocol()}' subprotocol`);this.disconnected=false;if(!this.isProtocolSupported()){e.log("Protocol is unsupported. Stopping monitor and disconnecting.");return this.close({allowReconnect:false})}},close(t){e.log("WebSocket onclose event");if(!this.disconnected){this.disconnected=true;this.monitor.recordDisconnect();return this.subscriptions.notifyAll("disconnected",{willAttemptReconnect:this.monitor.isRunning()})}},error(){e.log("WebSocket onerror event")}};const extend=function(t,e){if(e!=null)for(let n in e){const i=e[n];t[n]=i}return t};class Subscription{constructor(t,e={},n){this.consumer=t;this.identifier=JSON.stringify(e);extend(this,n)}perform(t,e={}){e.action=t;return this.send(e)}send(t){return this.consumer.send({command:"message",identifier:this.identifier,data:JSON.stringify(t)})}unsubscribe(){return this.consumer.subscriptions.remove(this)}}class SubscriptionGuarantor{constructor(t){this.subscriptions=t;this.pendingSubscriptions=[]}guarantee(t){if(this.pendingSubscriptions.indexOf(t)==-1){e.log(`SubscriptionGuarantor guaranteeing ${t.identifier}`);this.pendingSubscriptions.push(t)}else e.log(`SubscriptionGuarantor already guaranteeing ${t.identifier}`);this.startGuaranteeing()}forget(t){e.log(`SubscriptionGuarantor forgetting ${t.identifier}`);this.pendingSubscriptions=this.pendingSubscriptions.filter((e=>e!==t))}startGuaranteeing(){this.stopGuaranteeing();this.retrySubscribing()}stopGuaranteeing(){clearTimeout(this.retryTimeout)}retrySubscribing(){this.retryTimeout=setTimeout((()=>{this.subscriptions&&typeof this.subscriptions.subscribe==="function"&&this.pendingSubscriptions.map((t=>{e.log(`SubscriptionGuarantor resubscribing ${t.identifier}`);this.subscriptions.subscribe(t)}))}),500)}}class Subscriptions{constructor(t){this.consumer=t;this.guarantor=new SubscriptionGuarantor(this);this.subscriptions=[]}create(t,e){const n=t;const i=typeof n==="object"?n:{channel:n};const o=new Subscription(this.consumer,i,e);return this.add(o)}add(t){this.subscriptions.push(t);this.consumer.ensureActiveConnection();this.notify(t,"initialized");this.subscribe(t);return t}remove(t){this.forget(t);this.findAll(t.identifier).length||this.sendCommand(t,"unsubscribe");return t}reject(t){return this.findAll(t).map((t=>{this.forget(t);this.notify(t,"rejected");return t}))}forget(t){this.guarantor.forget(t);this.subscriptions=this.subscriptions.filter((e=>e!==t));return t}findAll(t){return this.subscriptions.filter((e=>e.identifier===t))}reload(){return this.subscriptions.map((t=>this.subscribe(t)))}notifyAll(t,...e){return this.subscriptions.map((n=>this.notify(n,t,...e)))}notify(t,e,...n){let i;i=typeof t==="string"?this.findAll(t):[t];return i.map((t=>typeof t[e]==="function"?t[e](...n):void 0))}subscribe(t){this.sendCommand(t,"subscribe")&&this.guarantor.guarantee(t)}confirmSubscription(t){e.log(`Subscription confirmed ${t}`);this.findAll(t).map((t=>this.guarantor.forget(t)))}sendCommand(t,e){const{identifier:n}=t;return this.consumer.send({command:e,identifier:n})}}class Consumer{constructor(t){this._url=t;this.subscriptions=new Subscriptions(this);this.connection=new Connection(this);this.subprotocols=[]}get url(){return createWebSocketURL(this._url)}send(t){return this.connection.send(t)}connect(){return this.connection.open()}disconnect(){return this.connection.close({allowReconnect:false})}ensureActiveConnection(){if(!this.connection.isActive())return this.connection.open()}addSubProtocol(t){this.subprotocols=[...this.subprotocols,t]}}function createWebSocketURL(t){typeof t==="function"&&(t=t());if(t&&!/^wss?:/i.test(t)){const e=document.createElement("a");e.href=t;e.href=e.href;e.protocol=e.protocol.replace("http","ws");return e.href}return t}function createConsumer(t=getConfig("url")||n.default_mount_path){return new Consumer(t)}function getConfig(t){const e=document.head.querySelector(`meta[name='action-cable-${t}']`);if(e)return e.getAttribute("content")}export{Connection,ConnectionMonitor,Consumer,n as INTERNAL,Subscription,SubscriptionGuarantor,Subscriptions,t as adapters,createConsumer,createWebSocketURL,getConfig,e as logger};
|
|
4
|
+
|
|
@@ -10,37 +10,41 @@ module Panda
|
|
|
10
10
|
before_save :downcase_email
|
|
11
11
|
|
|
12
12
|
# Scopes
|
|
13
|
-
scope :admin, -> { where(
|
|
13
|
+
scope :admin, -> { where(is_admin: true) }
|
|
14
14
|
|
|
15
15
|
def self.find_or_create_from_auth_hash(auth_hash)
|
|
16
16
|
user = find_by(email: auth_hash.info.email.downcase)
|
|
17
17
|
return user if user
|
|
18
18
|
|
|
19
|
-
# Parse name into first and last
|
|
20
|
-
full_name = auth_hash.info.name || "Unknown User"
|
|
21
|
-
name_parts = full_name.split(" ", 2)
|
|
22
|
-
|
|
23
19
|
create!(
|
|
24
20
|
email: auth_hash.info.email.downcase,
|
|
25
|
-
|
|
26
|
-
lastname: name_parts[1] || "",
|
|
21
|
+
name: auth_hash.info.name || "Unknown User",
|
|
27
22
|
image_url: auth_hash.info.image,
|
|
28
|
-
|
|
23
|
+
is_admin: User.count.zero? # First user is admin
|
|
29
24
|
)
|
|
30
25
|
end
|
|
31
26
|
|
|
32
27
|
def admin?
|
|
33
|
-
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def name
|
|
37
|
-
"#{firstname} #{lastname}".strip
|
|
28
|
+
is_admin?
|
|
38
29
|
end
|
|
39
30
|
|
|
40
31
|
def active_for_authentication?
|
|
41
32
|
true
|
|
42
33
|
end
|
|
43
34
|
|
|
35
|
+
def name
|
|
36
|
+
# Support both schema versions:
|
|
37
|
+
# - Main app: has 'name' column
|
|
38
|
+
# - Test app: has 'firstname' and 'lastname' columns
|
|
39
|
+
if respond_to?(:firstname) && respond_to?(:lastname)
|
|
40
|
+
"#{firstname} #{lastname}".strip
|
|
41
|
+
elsif self[:name].present?
|
|
42
|
+
self[:name]
|
|
43
|
+
else
|
|
44
|
+
email&.split("@")&.first || "Unknown User"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
44
48
|
private
|
|
45
49
|
|
|
46
50
|
def downcase_email
|
|
@@ -1,59 +1,42 @@
|
|
|
1
|
-
|
|
2
|
-
<
|
|
3
|
-
<
|
|
4
|
-
<
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
<
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
<body class="h-full">
|
|
31
|
-
<div class="flex h-full" id="panda-container">
|
|
32
|
-
<div class="absolute top-0 w-full lg:flex lg:fixed lg:inset-y-0 lg:z-50 lg:flex-col lg:w-72">
|
|
33
|
-
<div class="flex overflow-y-auto flex-col gap-y-5 px-4 pb-4 max-h-16 bg-gradient-to-br lg:max-h-full grow from-gray-900 to-gray-700">
|
|
34
|
-
<%= render "panda/core/admin/shared/sidebar" %>
|
|
35
|
-
<%= yield :admin_sidebar_extra %>
|
|
36
|
-
</div>
|
|
37
|
-
</div>
|
|
38
|
-
|
|
39
|
-
<div class="flex flex-col flex-1 mt-16 ml-0 lg:mt-0 lg:ml-72" id="panda-inner-container">
|
|
40
|
-
<section id="panda-main" class="flex flex-row h-full">
|
|
41
|
-
<div class="flex-1 h-full" id="panda-core-primary-content">
|
|
42
|
-
<%= render "panda/core/admin/shared/breadcrumbs" if respond_to?(:breadcrumbs) %>
|
|
43
|
-
<%= render "panda/core/admin/shared/flash" %>
|
|
44
|
-
|
|
45
|
-
<%= yield %>
|
|
46
|
-
|
|
47
|
-
<% if content_for?(:sidebar) %>
|
|
48
|
-
<%= render "panda/core/admin/shared/slideover" do %>
|
|
49
|
-
<%= yield :sidebar %>
|
|
50
|
-
<% end %>
|
|
51
|
-
<% end %>
|
|
1
|
+
<%= render "panda/core/shared/header", html_class: "h-full bg-white" %>
|
|
2
|
+
<div class="flex h-full" id="panda-container">
|
|
3
|
+
<div class="absolute top-0 w-full lg:flex lg:fixed lg:inset-y-0 lg:z-50 lg:flex-col lg:w-72">
|
|
4
|
+
<div class="flex overflow-y-auto flex-col gap-y-5 px-4 pb-4 max-h-16 bg-gradient-admin lg:max-h-full grow" data-transition-enter="transition-all ease-in-out duration-300" data-transition-enter-from="m-h-16" data-transition-enter-to="max-h-full" data-transition-leave="transition-all ease-in-out duration-300" data-transition-leave-from="max-h-full" data-transition-leave-to="max-h-16">
|
|
5
|
+
<%= render "panda/core/admin/shared/sidebar" %>
|
|
6
|
+
</div>
|
|
7
|
+
</div>
|
|
8
|
+
<div class="flex flex-col flex-1 mt-16 ml-0 lg:mt-0 lg:ml-72" id="panda-inner-container" <% if content_for :sidebar %> data-controller="toggle" data-action="keydown.esc->modal#close" tabindex="-1"<% end %>>
|
|
9
|
+
<section id="panda-main" class="flex flex-row h-full">
|
|
10
|
+
<div class="flex-1 h-full" id="panda-primary-content">
|
|
11
|
+
<%= render "panda/core/admin/shared/breadcrumbs" %>
|
|
12
|
+
<%= render "panda/core/admin/shared/flash" %>
|
|
13
|
+
<%= yield %>
|
|
14
|
+
</div>
|
|
15
|
+
<% if content_for :sidebar %>
|
|
16
|
+
<div data-toggle-target="toggleable" class="hidden flex absolute right-0 flex-col h-full bg-white divide-y divide-gray-200 shadow-xl basis-3/12"
|
|
17
|
+
data-transition-enter="transform transition ease-in-out duration-500"
|
|
18
|
+
data-transition-enter-from="translate-x-full"
|
|
19
|
+
data-transition-enter-to="translate-x-0"
|
|
20
|
+
data-transition-leave="transform transition ease-in-out duration-500"
|
|
21
|
+
data-transition-leave-from="translate-x-full"
|
|
22
|
+
data-transition-leave-to="translate-x-0"
|
|
23
|
+
id="slideover">
|
|
24
|
+
<div class="overflow-y-auto flex-1 h-0">
|
|
25
|
+
<div class="py-3 px-4 mb-4 bg-black">
|
|
26
|
+
<div class="flex justify-between items-center">
|
|
27
|
+
<h2 class="text-base font-semibold leading-6 text-white" id="slideover-title"><i class="mr-2 fa-solid fa-gear"></i> <%= yield :sidebar_title %> </h2>
|
|
28
|
+
<button type="button" data-action="click->toggle#toggle touch->toggle#toggle"><i class="font-bold text-white fa-solid fa-xmark right"></i></button>
|
|
29
|
+
</div>
|
|
52
30
|
</div>
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
31
|
+
<div class="flex flex-col flex-1 justify-between">
|
|
32
|
+
<div class="px-4 space-y-6">
|
|
33
|
+
<%= yield :sidebar %>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
56
37
|
</div>
|
|
57
|
-
|
|
58
|
-
</
|
|
59
|
-
</
|
|
38
|
+
<% end %>
|
|
39
|
+
</section>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
<%= render "panda/core/shared/footer" %>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<div class="mt-5">
|
|
2
|
+
<div class="p-6 bg-white shadow rounded-lg">
|
|
3
|
+
<h2 class="text-lg font-medium text-gray-900">Welcome to Panda Admin</h2>
|
|
4
|
+
<p class="mt-1 text-sm text-gray-500">Manage your application from this central dashboard.</p>
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
<div class="mt-6 grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3">
|
|
8
|
+
<% if defined?(Panda::CMS) %>
|
|
9
|
+
<div class="bg-white overflow-hidden shadow rounded-lg">
|
|
10
|
+
<div class="px-4 py-5 sm:p-6">
|
|
11
|
+
<div class="flex items-center">
|
|
12
|
+
<div class="flex-shrink-0">
|
|
13
|
+
<i class="fa-solid fa-file-lines text-3xl text-gray-400"></i>
|
|
14
|
+
</div>
|
|
15
|
+
<div class="ml-5 w-0 flex-1">
|
|
16
|
+
<dt class="text-sm font-medium text-gray-500 truncate">Content Management</dt>
|
|
17
|
+
<dd class="mt-1 text-lg font-semibold text-gray-900">
|
|
18
|
+
<%= link_to "Manage CMS", panda_cms.admin_cms_dashboard_path, class: "text-indigo-600 hover:text-indigo-900" %>
|
|
19
|
+
</dd>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
<div class="mt-3">
|
|
23
|
+
<p class="text-sm text-gray-500">Pages, posts, menus, and content blocks</p>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
<% end %>
|
|
28
|
+
|
|
29
|
+
<div class="bg-white overflow-hidden shadow rounded-lg">
|
|
30
|
+
<div class="px-4 py-5 sm:p-6">
|
|
31
|
+
<div class="flex items-center">
|
|
32
|
+
<div class="flex-shrink-0">
|
|
33
|
+
<i class="fa-regular fa-user text-3xl text-gray-400"></i>
|
|
34
|
+
</div>
|
|
35
|
+
<div class="ml-5 w-0 flex-1">
|
|
36
|
+
<dt class="text-sm font-medium text-gray-500 truncate">My Profile</dt>
|
|
37
|
+
<dd class="mt-1 text-lg font-semibold text-gray-900">
|
|
38
|
+
<%= link_to "Edit Profile", edit_admin_my_profile_path, class: "text-indigo-600 hover:text-indigo-900" %>
|
|
39
|
+
</dd>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
<div class="mt-3">
|
|
43
|
+
<p class="text-sm text-gray-500">Update your personal information</p>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<% # Hook for additional dashboard cards %>
|
|
49
|
+
<% if Panda::Core.config.respond_to?(:admin_dashboard_cards) %>
|
|
50
|
+
<% cards = Panda::Core.config.admin_dashboard_cards&.call(current_user) %>
|
|
51
|
+
<% cards&.each do |card| %>
|
|
52
|
+
<div class="bg-white overflow-hidden shadow rounded-lg">
|
|
53
|
+
<div class="px-4 py-5 sm:p-6">
|
|
54
|
+
<div class="flex items-center">
|
|
55
|
+
<div class="flex-shrink-0">
|
|
56
|
+
<i class="<%= card[:icon] %> text-3xl text-gray-400"></i>
|
|
57
|
+
</div>
|
|
58
|
+
<div class="ml-5 w-0 flex-1">
|
|
59
|
+
<dt class="text-sm font-medium text-gray-500 truncate"><%= card[:title] %></dt>
|
|
60
|
+
<dd class="mt-1 text-lg font-semibold text-gray-900">
|
|
61
|
+
<%= link_to card[:link_text], card[:path], class: "text-indigo-600 hover:text-indigo-900" %>
|
|
62
|
+
</dd>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
<div class="mt-3">
|
|
66
|
+
<p class="text-sm text-gray-500"><%= card[:description] %></p>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
<% end %>
|
|
71
|
+
<% end %>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
<% container.with_heading(text: "Dashboard", level: 1) %>
|
|
4
4
|
|
|
5
5
|
<% # Hook for dashboard widgets %>
|
|
6
|
-
<% if Panda::Core.
|
|
7
|
-
<% widgets = Panda::Core.
|
|
6
|
+
<% if Panda::Core.config.admin_dashboard_widgets %>
|
|
7
|
+
<% widgets = Panda::Core.config.admin_dashboard_widgets.call(current_user) %>
|
|
8
8
|
<% if widgets && widgets.any? %>
|
|
9
9
|
<div class="grid grid-cols-1 gap-5 mt-5 sm:grid-cols-3">
|
|
10
10
|
<% widgets.each do |widget| %>
|
|
@@ -12,16 +12,10 @@
|
|
|
12
12
|
<% end %>
|
|
13
13
|
</div>
|
|
14
14
|
<% else %>
|
|
15
|
-
|
|
16
|
-
<h2 class="text-lg font-medium text-gray-900">Welcome to Panda Core Admin</h2>
|
|
17
|
-
<p class="mt-1 text-sm text-gray-500">Configure dashboard widgets to display custom content here.</p>
|
|
18
|
-
</div>
|
|
15
|
+
<%= render "panda/core/admin/dashboard/default_content" %>
|
|
19
16
|
<% end %>
|
|
20
17
|
<% else %>
|
|
21
|
-
|
|
22
|
-
<h2 class="text-lg font-medium text-gray-900">Welcome to Panda Core Admin</h2>
|
|
23
|
-
<p class="mt-1 text-sm text-gray-500">No dashboard widgets configured.</p>
|
|
24
|
-
</div>
|
|
18
|
+
<%= render "panda/core/admin/dashboard/default_content" %>
|
|
25
19
|
<% end %>
|
|
26
20
|
<% end %>
|
|
27
21
|
</div>
|
|
@@ -1,49 +1,35 @@
|
|
|
1
1
|
<%= render Panda::Core::Admin::ContainerComponent.new do |component| %>
|
|
2
|
-
<% component.
|
|
2
|
+
<% component.heading(text: "My Profile", level: 1) %>
|
|
3
3
|
|
|
4
4
|
<%= form_with model: user,
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
<div class="mb-4 p-4 bg-red-50 border border-red-200 rounded-md">
|
|
11
|
-
<div class="text-sm text-red-600">
|
|
12
|
-
<% user.errors.full_messages.each do |message| %>
|
|
13
|
-
<p><%= message %></p>
|
|
14
|
-
<% end %>
|
|
15
|
-
</div>
|
|
16
|
-
</div>
|
|
17
|
-
<% end %>
|
|
5
|
+
url: admin_my_profile_path,
|
|
6
|
+
method: :patch,
|
|
7
|
+
local: true,
|
|
8
|
+
data: { controller: "theme-form" } do |f| %>
|
|
9
|
+
<%= render Panda::Core::Admin::FormErrorComponent.new(model: user) %>
|
|
18
10
|
|
|
19
11
|
<div class="space-y-4">
|
|
20
12
|
<div class="field">
|
|
21
|
-
<%= f.label :
|
|
22
|
-
<%= f.text_field :
|
|
23
|
-
</div>
|
|
24
|
-
|
|
25
|
-
<div class="field">
|
|
26
|
-
<%= f.label :lastname, "Last Name", class: "block text-sm font-medium text-gray-700" %>
|
|
27
|
-
<%= f.text_field :lastname, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" %>
|
|
13
|
+
<%= f.label :name %>
|
|
14
|
+
<%= f.text_field :name %>
|
|
28
15
|
</div>
|
|
29
16
|
|
|
30
17
|
<div class="field">
|
|
31
|
-
<%= f.label :email
|
|
32
|
-
<%= f.email_field :email
|
|
18
|
+
<%= f.label :email %>
|
|
19
|
+
<%= f.email_field :email %>
|
|
33
20
|
</div>
|
|
34
21
|
|
|
35
22
|
<div class="field">
|
|
36
|
-
<%= f.label :current_theme, "Theme"
|
|
23
|
+
<%= f.label :current_theme, "Theme" %>
|
|
37
24
|
<%= f.select :current_theme,
|
|
38
|
-
options_for_select(Panda::Core.
|
|
25
|
+
options_for_select(Panda::Core.config.available_themes || [["Default", "default"], ["Sky", "sky"]], user.current_theme),
|
|
39
26
|
{},
|
|
40
|
-
class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm",
|
|
41
27
|
data: { action: "change->theme-form#updateTheme" } %>
|
|
42
28
|
</div>
|
|
43
29
|
</div>
|
|
44
30
|
|
|
45
31
|
<%= f.submit "Update Profile",
|
|
46
|
-
class: "btn btn-primary mt-
|
|
32
|
+
class: "btn btn-primary mt-4",
|
|
47
33
|
data: { disable_with: "Saving..." } %>
|
|
48
34
|
<% end %>
|
|
49
35
|
<% end %>
|
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
<div class="flex flex-col justify-center py-12 px-6 min-h-full text-center lg:px-8">
|
|
2
2
|
<div class="text-center sm:mx-auto sm:w-full sm:max-w-sm">
|
|
3
|
-
<% if Panda::Core.
|
|
4
|
-
<img src="<%= Panda::Core.
|
|
3
|
+
<% if Panda::Core.config.login_logo_path %>
|
|
4
|
+
<img src="<%= Panda::Core.config.login_logo_path %>" class="py-2 mx-auto w-auto h-32">
|
|
5
5
|
<% end %>
|
|
6
|
-
<h2 class="mt-10 mb-6 text-2xl font-bold text-center text-
|
|
7
|
-
<%= Panda::Core.
|
|
6
|
+
<h2 class="mt-10 mb-6 text-2xl font-bold text-center text-white">
|
|
7
|
+
<%= Panda::Core.config.login_page_title || "Sign in to your account" %>
|
|
8
8
|
</h2>
|
|
9
9
|
</div>
|
|
10
|
-
<% if @providers&.any? || Panda::Core.
|
|
11
|
-
<% providers = @providers || Panda::Core.
|
|
10
|
+
<% if @providers&.any? || Panda::Core.config.authentication_providers.any? %>
|
|
11
|
+
<% providers = @providers || Panda::Core.config.authentication_providers.keys %>
|
|
12
12
|
<% providers.each do |provider| %>
|
|
13
|
+
<% provider_config = Panda::Core.config.authentication_providers[provider] %>
|
|
14
|
+
<% provider_name = provider_config&.dig(:name) || provider.to_s.humanize %>
|
|
15
|
+
<% provider_path = provider_config&.dig(:path_name) || provider %>
|
|
13
16
|
<div class="mt-4 text-center sm:mx-auto sm:w-full sm:max-w-sm">
|
|
14
|
-
<%= form_tag "#{Panda::Core.
|
|
15
|
-
<button type="submit" id="button-sign-in-<%=
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
<% end %>
|
|
19
|
-
Sign in with <%= provider.to_s.humanize %>
|
|
17
|
+
<%= form_tag "#{Panda::Core.config.admin_path}/auth/#{provider_path}", method: "post", data: {turbo: false} do %>
|
|
18
|
+
<button type="submit" id="button-sign-in-<%= provider_path %>" class="inline-flex gap-x-2 items-center py-2.5 px-3.5 mx-auto mb-4 bg-white text-gray-900 rounded-md border min-w-56 border-neutral-400 hover:bg-gray-50">
|
|
19
|
+
<i class="fa-brands fa-<%= oauth_provider_icon(provider) %> text-xl mr-1"></i>
|
|
20
|
+
Sign in with <%= provider_name %>
|
|
20
21
|
</button>
|
|
21
22
|
<% end %>
|
|
22
23
|
</div>
|
|
@@ -1,35 +1,28 @@
|
|
|
1
|
-
<%
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
</svg>
|
|
11
|
-
<% end %>
|
|
12
|
-
|
|
13
|
-
<% if breadcrumb.path && index < @breadcrumbs.length - 1 %>
|
|
14
|
-
<%= link_to breadcrumb.label, breadcrumb.path, class: "inline-flex items-center text-sm font-medium text-gray-700 hover:text-blue-600" %>
|
|
15
|
-
<% else %>
|
|
16
|
-
<span class="text-sm font-medium text-gray-500"><%= breadcrumb.label %></span>
|
|
17
|
-
<% end %>
|
|
18
|
-
</li>
|
|
19
|
-
<% end %>
|
|
20
|
-
</ol>
|
|
21
|
-
</nav>
|
|
22
|
-
|
|
23
|
-
<% if content_for?(:sidebar) %>
|
|
24
|
-
<div class="px-4 py-3 border-b border-gray-200 bg-gray-50 text-gray-700" tabindex="-1" data-controller="toggle">
|
|
25
|
-
<a href="#" id="slideover-toggle" data-action="click->toggle#toggle touch->toggle#toggle" class="inline-flex items-center text-sm font-medium text-gray-700 hover:text-blue-600">
|
|
26
|
-
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
27
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
|
|
28
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
|
29
|
-
</svg>
|
|
30
|
-
<%= yield(:sidebar_title) || "Settings" %>
|
|
1
|
+
<% breadcrumb_styles = "text-black/60 hover:text-black/80" %>
|
|
2
|
+
|
|
3
|
+
<div class="flex mt-1 -mb-1">
|
|
4
|
+
<nav aria-label="Breadcrumb" id="panda-breadcrumbs" class="grow">
|
|
5
|
+
<ol role="list" class="px-4 w-full sm:px-6">
|
|
6
|
+
<li class="inline-block">
|
|
7
|
+
<a href="<%= Panda::Core.config.admin_path %>" class="<%= breadcrumb_styles %>">
|
|
8
|
+
<i class="fa fa-solid fa-house"></i>
|
|
9
|
+
<span class="sr-only">Home</span>
|
|
31
10
|
</a>
|
|
32
|
-
</
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
11
|
+
</li>
|
|
12
|
+
<% breadcrumbs.each do |crumb| %>
|
|
13
|
+
<li class="inline-block">
|
|
14
|
+
<i class="fa fa-regular fa-chevron-right font-light <%= breadcrumb_styles %> py-5 px-2"></i>
|
|
15
|
+
<a href="<%= crumb.path %>" class="text-sm font-normal <%= breadcrumb_styles %>"><%= crumb.name %></a>
|
|
16
|
+
</li>
|
|
17
|
+
<% end %>
|
|
18
|
+
</ol>
|
|
19
|
+
</nav>
|
|
20
|
+
|
|
21
|
+
<% if content_for :sidebar %>
|
|
22
|
+
<div class="pt-4 pr-8 text-black/80" tabindex="-1" data-controller="toggle">
|
|
23
|
+
<button type="button" id="slideover-toggle" data-action="click->toggle#toggle touch->toggle#toggle" class="text-sm font-light">
|
|
24
|
+
<i class="mr-1 fa-light fa-gear"></i> <%= yield :sidebar_title %>
|
|
25
|
+
</button>
|
|
26
|
+
</div>
|
|
27
|
+
<% end %>
|
|
28
|
+
</div>
|
|
@@ -1,31 +1,5 @@
|
|
|
1
1
|
<% if flash.any? %>
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
<div class="flex-shrink-0">
|
|
7
|
-
<% if type == "success" %>
|
|
8
|
-
<i class="fa-regular fa-check-circle text-green-500"></i>
|
|
9
|
-
<% elsif type == "error" %>
|
|
10
|
-
<i class="fa-regular fa-times-circle text-red-500"></i>
|
|
11
|
-
<% elsif type == "warning" %>
|
|
12
|
-
<i class="fa-regular fa-exclamation-triangle text-yellow-500"></i>
|
|
13
|
-
<% else %>
|
|
14
|
-
<i class="fa-regular fa-info-circle text-blue-500"></i>
|
|
15
|
-
<% end %>
|
|
16
|
-
</div>
|
|
17
|
-
<div class="ml-3 w-0 flex-1">
|
|
18
|
-
<p class="text-sm font-medium text-gray-900">
|
|
19
|
-
<%= message %>
|
|
20
|
-
</p>
|
|
21
|
-
</div>
|
|
22
|
-
<div class="ml-4 flex-shrink-0 flex">
|
|
23
|
-
<button type="button" class="bg-white rounded-md inline-flex text-gray-400 hover:text-gray-500" data-action="click->flash#close">
|
|
24
|
-
<i class="fa-regular fa-times"></i>
|
|
25
|
-
</button>
|
|
26
|
-
</div>
|
|
27
|
-
</div>
|
|
28
|
-
</div>
|
|
29
|
-
<% end %>
|
|
30
|
-
</div>
|
|
31
|
-
<% end %>
|
|
2
|
+
<% flash.each do |kind, message| %>
|
|
3
|
+
<%= render Panda::Core::Admin::FlashMessageComponent.new(kind: kind.to_sym, message: message) %>
|
|
4
|
+
<% end %>
|
|
5
|
+
<% end %>
|
|
@@ -1,27 +1,43 @@
|
|
|
1
1
|
<nav class="flex flex-col flex-1">
|
|
2
|
-
<ul role="list" class="flex flex-col flex-1
|
|
3
|
-
<
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
2
|
+
<ul role="list" class="flex flex-col flex-1">
|
|
3
|
+
<a class="block p-0 mt-4 mb-4 ml-2 text-xl font-medium text-white"><%= Panda::Core.config.admin_title || "Panda Admin" %></a>
|
|
4
|
+
<% Panda::Core.config.admin_navigation_items&.call(current_user)&.each do |item| %>
|
|
5
|
+
<li>
|
|
6
|
+
<%
|
|
7
|
+
# Exact match for dashboard, starts_with for others
|
|
8
|
+
# Check if current path matches this nav item
|
|
9
|
+
is_active = if request.path == item[:path]
|
|
10
|
+
true
|
|
11
|
+
elsif request.path.starts_with?(item[:path] + "/")
|
|
12
|
+
true
|
|
13
|
+
else
|
|
14
|
+
false
|
|
15
|
+
end
|
|
16
|
+
%>
|
|
17
|
+
<%= link_to item[:path], class: "#{is_active ? "bg-mid text-white relative flex items-center transition-all py-3 px-2 mb-2 rounded-md group gap-x-3 text-base leading-6 font-normal" : "text-white hover:bg-mid/60 transition-all group flex items-center gap-x-3 py-3 px-2 mb-2 rounded-md text-base leading-6 font-normal"}" do %>
|
|
18
|
+
<span class="text-center w-6"><i class="<%= item[:icon] %> text-xl fa-fw"></i></span>
|
|
19
|
+
<span><%= item[:label] %></span>
|
|
12
20
|
<% end %>
|
|
13
|
-
</
|
|
21
|
+
</li>
|
|
22
|
+
<% end %>
|
|
23
|
+
<li>
|
|
24
|
+
<%= button_to panda_core.admin_logout_path, method: :delete, id: "logout-link", data: { turbo: false }, class: "text-white hover:bg-mid/60 transition-all group flex items-center gap-x-3 py-3 px-2 mb-2 rounded-md text-base leading-6 font-normal w-full" do %>
|
|
25
|
+
<span class="text-center w-6"><i class="text-xl fa-solid fa-door-open fa-fw"></i></span>
|
|
26
|
+
<span>Logout</span>
|
|
27
|
+
<% end %>
|
|
14
28
|
</li>
|
|
15
|
-
|
|
16
29
|
<li class="mt-auto">
|
|
17
|
-
|
|
18
|
-
<% if current_user %>
|
|
19
|
-
<span class="
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
<% end %>
|
|
30
|
+
<%= link_to panda_core.edit_admin_my_profile_path, class: "text-white hover:bg-mid/60 transition-all group flex items-center gap-x-3 py-3 px-2 mb-2 rounded-md text-base leading-6 font-normal w-full", title: "Edit my Profile" do %>
|
|
31
|
+
<% if !current_user.image_url.to_s.empty? %>
|
|
32
|
+
<span class="text-center w-6"><img src="<%= current_user.image_url %>" class="w-auto h-7 rounded-full"></span>
|
|
33
|
+
<% else %>
|
|
34
|
+
<span class="text-center w-6"><i class="text-xl fa-regular fa-circle-user fa-fw"></i></span>
|
|
23
35
|
<% end %>
|
|
24
|
-
|
|
36
|
+
<span><%= current_user.name %></span>
|
|
37
|
+
<% end %>
|
|
38
|
+
</li>
|
|
39
|
+
<li class="px-2 py-3">
|
|
40
|
+
<span class="text-xs text-white">Panda Core v<%= Panda::Core::VERSION %></span>
|
|
25
41
|
</li>
|
|
26
42
|
</ul>
|
|
27
|
-
</nav>
|
|
43
|
+
</nav>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html data-theme="<%= Panda::Core::Current&.user&.current_theme || Panda::Core.config.default_theme %>" class="<%= local_assigns[:html_class] || "" %>">
|
|
3
|
+
<head>
|
|
4
|
+
<title><%= content_for?(:title) ? yield(:title) : (Panda::Core.config.admin_title || "Panda Admin") %></title>
|
|
5
|
+
<%= csrf_meta_tags %>
|
|
6
|
+
<%= csp_meta_tag %>
|
|
7
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@7.1.0/css/all.min.css">
|
|
8
|
+
<%= panda_core_stylesheet %>
|
|
9
|
+
<%= panda_core_javascript %>
|
|
10
|
+
|
|
11
|
+
<% if defined?(Panda::CMS) && controller.class.name.start_with?("Panda::CMS") %>
|
|
12
|
+
<!-- CMS Assets -->
|
|
13
|
+
<%= panda_cms_javascript %>
|
|
14
|
+
<%= render "panda/cms/shared/favicons" %>
|
|
15
|
+
<% end %>
|
|
16
|
+
|
|
17
|
+
<%= yield :head %>
|
|
18
|
+
</head>
|
|
19
|
+
<body class="overflow-hidden h-full <%= local_assigns[:body_class] || "" %>" data-environment="<%= Rails.env %>">
|
data/config/importmap.rb
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Panda Core application and controllers
|
|
4
|
+
pin "panda/core/application", to: "panda/core/application.js"
|
|
5
|
+
pin "panda/core/controllers/index", to: "panda/core/controllers/index.js"
|
|
6
|
+
pin_all_from Panda::Core::Engine.root.join("app/javascript/panda/core/controllers"), under: "panda/core/controllers"
|
|
7
|
+
|
|
8
|
+
# Base JavaScript dependencies for Panda Core (vendored for reliability)
|
|
9
|
+
pin "@hotwired/stimulus", to: "panda/core/vendor/@hotwired--stimulus.js", preload: true # @3.2.2
|
|
10
|
+
pin "@hotwired/turbo", to: "panda/core/vendor/@hotwired--turbo.js", preload: true # @8.0.18
|
|
11
|
+
pin "@rails/actioncable/src", to: "panda/core/vendor/@rails--actioncable--src.js", preload: true # @8.0.201
|
|
12
|
+
pin "tailwindcss-stimulus-components", to: "panda/core/tailwindcss-stimulus-components.js" # @6.1.3
|
|
13
|
+
|
|
14
|
+
# Font Awesome icons (from CDN)
|
|
15
|
+
pin "@fortawesome/fontawesome-free", to: "https://ga.jspm.io/npm:@fortawesome/fontawesome-free@7.1.0/js/all.js"
|