@sonicjs-cms/core 2.3.13 → 2.3.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/{chunk-RP66TPEJ.js → chunk-4M2UOUXV.js} +415 -320
  2. package/dist/chunk-4M2UOUXV.js.map +1 -0
  3. package/dist/{chunk-ARLXQU2S.cjs → chunk-5OLN5JO3.cjs} +560 -465
  4. package/dist/chunk-5OLN5JO3.cjs.map +1 -0
  5. package/dist/{chunk-W4CE7XME.cjs → chunk-63LV4YVI.cjs} +2 -2
  6. package/dist/{chunk-W4CE7XME.cjs.map → chunk-63LV4YVI.cjs.map} +1 -1
  7. package/dist/{chunk-FHCN7KR2.js → chunk-67SKO5RQ.js} +3 -3
  8. package/dist/{chunk-FHCN7KR2.js.map → chunk-67SKO5RQ.js.map} +1 -1
  9. package/dist/{chunk-VMEBHBYY.js → chunk-72I2MOSH.js} +2 -2
  10. package/dist/{chunk-VMEBHBYY.js.map → chunk-72I2MOSH.js.map} +1 -1
  11. package/dist/{chunk-2NTBZ2Y7.js → chunk-A27RBGBA.js} +3 -3
  12. package/dist/{chunk-2NTBZ2Y7.js.map → chunk-A27RBGBA.js.map} +1 -1
  13. package/dist/{chunk-F56JKQTA.js → chunk-AVPUX57O.js} +3 -3
  14. package/dist/{chunk-F56JKQTA.js.map → chunk-AVPUX57O.js.map} +1 -1
  15. package/dist/{chunk-MF7DWI5P.cjs → chunk-AZLU3ROK.cjs} +4 -2
  16. package/dist/chunk-AZLU3ROK.cjs.map +1 -0
  17. package/dist/{chunk-W2IAEG4W.cjs → chunk-H2X4BFCW.cjs} +3 -3
  18. package/dist/{chunk-W2IAEG4W.cjs.map → chunk-H2X4BFCW.cjs.map} +1 -1
  19. package/dist/{chunk-5NCBFP37.cjs → chunk-QG3YQKL4.cjs} +4 -4
  20. package/dist/{chunk-5NCBFP37.cjs.map → chunk-QG3YQKL4.cjs.map} +1 -1
  21. package/dist/{chunk-DN45O5XV.js → chunk-V5LBQN3I.js} +4 -2
  22. package/dist/chunk-V5LBQN3I.js.map +1 -0
  23. package/dist/{chunk-XR6XACXJ.cjs → chunk-YIXSSJWD.cjs} +5 -5
  24. package/dist/{chunk-XR6XACXJ.cjs.map → chunk-YIXSSJWD.cjs.map} +1 -1
  25. package/dist/index.cjs +1111 -87
  26. package/dist/index.cjs.map +1 -1
  27. package/dist/index.d.cts +1 -1
  28. package/dist/index.d.ts +1 -1
  29. package/dist/index.js +1034 -10
  30. package/dist/index.js.map +1 -1
  31. package/dist/middleware.cjs +23 -23
  32. package/dist/middleware.js +2 -2
  33. package/dist/migrations-H3Q5FZGZ.js +4 -0
  34. package/dist/{migrations-43GTELB5.js.map → migrations-H3Q5FZGZ.js.map} +1 -1
  35. package/dist/migrations-VN5VTX3C.cjs +13 -0
  36. package/dist/{migrations-ZAYXZXON.cjs.map → migrations-VN5VTX3C.cjs.map} +1 -1
  37. package/dist/{plugin-manifest-BCMx9CAq.d.cts → plugin-manifest-Dpy8wxIB.d.cts} +2 -2
  38. package/dist/{plugin-manifest-BCMx9CAq.d.ts → plugin-manifest-Dpy8wxIB.d.ts} +2 -2
  39. package/dist/routes.cjs +25 -25
  40. package/dist/routes.js +5 -5
  41. package/dist/services.cjs +2 -2
  42. package/dist/services.js +1 -1
  43. package/dist/templates.cjs +17 -17
  44. package/dist/templates.js +2 -2
  45. package/dist/types.d.cts +1 -1
  46. package/dist/types.d.ts +1 -1
  47. package/dist/utils.cjs +11 -11
  48. package/dist/utils.js +1 -1
  49. package/package.json +1 -1
  50. package/dist/chunk-ARLXQU2S.cjs.map +0 -1
  51. package/dist/chunk-DN45O5XV.js.map +0 -1
  52. package/dist/chunk-MF7DWI5P.cjs.map +0 -1
  53. package/dist/chunk-RP66TPEJ.js.map +0 -1
  54. package/dist/migrations-43GTELB5.js +0 -4
  55. package/dist/migrations-ZAYXZXON.cjs +0 -13
@@ -1,9 +1,9 @@
1
1
  import { getCacheService, CACHE_CONFIGS, getLogger, SettingsService } from './chunk-3YNNVSMC.js';
2
- import { requireAuth, isPluginActive, requireRole, AuthManager, logActivity } from './chunk-2NTBZ2Y7.js';
2
+ import { requireAuth, isPluginActive, requireRole, AuthManager, logActivity } from './chunk-A27RBGBA.js';
3
3
  import { PluginService } from './chunk-SGAG6FD3.js';
4
- import { MigrationService } from './chunk-VMEBHBYY.js';
5
- import { init_admin_layout_catalyst_template, renderDesignPage, renderCheckboxPage, renderTestimonialsList, renderCodeExamplesList, renderAlert, renderTable, renderPagination, renderConfirmationDialog, getConfirmationDialogScript, renderAdminLayoutCatalyst, renderAdminLayout, adminLayoutV2, renderForm } from './chunk-DN45O5XV.js';
6
- import { QueryFilterBuilder, sanitizeInput, getCoreVersion, escapeHtml } from './chunk-FHCN7KR2.js';
4
+ import { MigrationService } from './chunk-72I2MOSH.js';
5
+ import { init_admin_layout_catalyst_template, renderDesignPage, renderCheckboxPage, renderTestimonialsList, renderCodeExamplesList, renderAlert, renderTable, renderPagination, renderConfirmationDialog, getConfirmationDialogScript, renderAdminLayoutCatalyst, renderAdminLayout, adminLayoutV2, renderForm } from './chunk-V5LBQN3I.js';
6
+ import { QueryFilterBuilder, sanitizeInput, getCoreVersion, escapeHtml } from './chunk-67SKO5RQ.js';
7
7
  import { metricsTracker } from './chunk-FICTAGD4.js';
8
8
  import { Hono } from 'hono';
9
9
  import { cors } from 'hono/cors';
@@ -1720,7 +1720,7 @@ adminApiRoutes.delete("/collections/:id", async (c) => {
1720
1720
  });
1721
1721
  adminApiRoutes.get("/migrations/status", async (c) => {
1722
1722
  try {
1723
- const { MigrationService: MigrationService2 } = await import('./migrations-43GTELB5.js');
1723
+ const { MigrationService: MigrationService2 } = await import('./migrations-H3Q5FZGZ.js');
1724
1724
  const db = c.env.DB;
1725
1725
  const migrationService = new MigrationService2(db);
1726
1726
  const status = await migrationService.getMigrationStatus();
@@ -1745,7 +1745,7 @@ adminApiRoutes.post("/migrations/run", async (c) => {
1745
1745
  error: "Unauthorized. Admin access required."
1746
1746
  }, 403);
1747
1747
  }
1748
- const { MigrationService: MigrationService2 } = await import('./migrations-43GTELB5.js');
1748
+ const { MigrationService: MigrationService2 } = await import('./migrations-H3Q5FZGZ.js');
1749
1749
  const db = c.env.DB;
1750
1750
  const migrationService = new MigrationService2(db);
1751
1751
  const result = await migrationService.runPendingMigrations();
@@ -1764,7 +1764,7 @@ adminApiRoutes.post("/migrations/run", async (c) => {
1764
1764
  });
1765
1765
  adminApiRoutes.get("/migrations/validate", async (c) => {
1766
1766
  try {
1767
- const { MigrationService: MigrationService2 } = await import('./migrations-43GTELB5.js');
1767
+ const { MigrationService: MigrationService2 } = await import('./migrations-H3Q5FZGZ.js');
1768
1768
  const db = c.env.DB;
1769
1769
  const migrationService = new MigrationService2(db);
1770
1770
  const validation = await migrationService.validateSchema();
@@ -2117,6 +2117,27 @@ function renderRegisterPage(data) {
2117
2117
  </html>
2118
2118
  `;
2119
2119
  }
2120
+ async function isRegistrationEnabled(db) {
2121
+ try {
2122
+ const plugin = await db.prepare("SELECT settings FROM plugins WHERE id = ?").bind("core-auth").first();
2123
+ if (plugin?.settings) {
2124
+ const settings = JSON.parse(plugin.settings);
2125
+ const enabled = settings?.registration?.enabled;
2126
+ return enabled !== false && enabled !== 0;
2127
+ }
2128
+ return true;
2129
+ } catch {
2130
+ return true;
2131
+ }
2132
+ }
2133
+ async function isFirstUserRegistration(db) {
2134
+ try {
2135
+ const result = await db.prepare("SELECT COUNT(*) as count FROM users").first();
2136
+ return result?.count === 0;
2137
+ } catch {
2138
+ return false;
2139
+ }
2140
+ }
2120
2141
  var baseRegistrationSchema = z.object({
2121
2142
  email: z.string().email("Valid email is required"),
2122
2143
  password: z.string().min(8, "Password must be at least 8 characters"),
@@ -2168,7 +2189,15 @@ authRoutes.get("/login", async (c) => {
2168
2189
  }
2169
2190
  return c.html(renderLoginPage(pageData, demoLoginActive));
2170
2191
  });
2171
- authRoutes.get("/register", (c) => {
2192
+ authRoutes.get("/register", async (c) => {
2193
+ const db = c.env.DB;
2194
+ const isFirstUser = await isFirstUserRegistration(db);
2195
+ if (!isFirstUser) {
2196
+ const registrationEnabled = await isRegistrationEnabled(db);
2197
+ if (!registrationEnabled) {
2198
+ return c.redirect("/auth/login?error=Registration is currently disabled");
2199
+ }
2200
+ }
2172
2201
  const error = c.req.query("error");
2173
2202
  const pageData = {
2174
2203
  error: error || void 0
@@ -2184,6 +2213,13 @@ authRoutes.post(
2184
2213
  async (c) => {
2185
2214
  try {
2186
2215
  const db = c.env.DB;
2216
+ const isFirstUser = await isFirstUserRegistration(db);
2217
+ if (!isFirstUser) {
2218
+ const registrationEnabled = await isRegistrationEnabled(db);
2219
+ if (!registrationEnabled) {
2220
+ return c.json({ error: "Registration is currently disabled" }, 403);
2221
+ }
2222
+ }
2187
2223
  let requestData;
2188
2224
  try {
2189
2225
  requestData = await c.req.json();
@@ -2376,6 +2412,17 @@ authRoutes.post("/refresh", requireAuth(), async (c) => {
2376
2412
  authRoutes.post("/register/form", async (c) => {
2377
2413
  try {
2378
2414
  const db = c.env.DB;
2415
+ const isFirstUser = await isFirstUserRegistration(db);
2416
+ if (!isFirstUser) {
2417
+ const registrationEnabled = await isRegistrationEnabled(db);
2418
+ if (!registrationEnabled) {
2419
+ return c.html(html`
2420
+ <div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
2421
+ Registration is currently disabled. Please contact an administrator.
2422
+ </div>
2423
+ `);
2424
+ }
2425
+ }
2379
2426
  const formData = await c.req.formData();
2380
2427
  const requestData = {
2381
2428
  email: formData.get("email"),
@@ -2409,6 +2456,7 @@ authRoutes.post("/register/form", async (c) => {
2409
2456
  `);
2410
2457
  }
2411
2458
  const passwordHash = await AuthManager.hashPassword(password);
2459
+ const role = isFirstUser ? "admin" : "viewer";
2412
2460
  const userId = crypto.randomUUID();
2413
2461
  const now = /* @__PURE__ */ new Date();
2414
2462
  await db.prepare(`
@@ -2421,14 +2469,13 @@ authRoutes.post("/register/form", async (c) => {
2421
2469
  firstName,
2422
2470
  lastName,
2423
2471
  passwordHash,
2424
- "admin",
2425
- // First user gets admin role
2472
+ role,
2426
2473
  1,
2427
2474
  // is_active
2428
2475
  now.getTime(),
2429
2476
  now.getTime()
2430
2477
  ).run();
2431
- const token = await AuthManager.generateToken(userId, normalizedEmail, "admin");
2478
+ const token = await AuthManager.generateToken(userId, normalizedEmail, role);
2432
2479
  setCookie(c, "auth_token", token, {
2433
2480
  httpOnly: true,
2434
2481
  secure: false,
@@ -2437,12 +2484,13 @@ authRoutes.post("/register/form", async (c) => {
2437
2484
  maxAge: 60 * 60 * 24
2438
2485
  // 24 hours
2439
2486
  });
2487
+ const redirectUrl = role === "admin" ? "/admin/dashboard" : "/admin/dashboard";
2440
2488
  return c.html(html`
2441
2489
  <div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded">
2442
- Account created successfully! Redirecting to admin dashboard...
2490
+ Account created successfully! Redirecting...
2443
2491
  <script>
2444
2492
  setTimeout(() => {
2445
- window.location.href = '/admin/dashboard';
2493
+ window.location.href = '${redirectUrl}';
2446
2494
  }, 2000);
2447
2495
  </script>
2448
2496
  </div>
@@ -4426,6 +4474,13 @@ function getMDXEditorInitScript(config) {
4426
4474
  // Store reference to editor instance
4427
4475
  textarea.easyMDEInstance = easyMDE;
4428
4476
 
4477
+ // Sync changes back to textarea
4478
+ easyMDE.codemirror.on("change", () => {
4479
+ textarea.value = easyMDE.value();
4480
+ textarea.dispatchEvent(new Event("input", { bubbles: true }));
4481
+ textarea.dispatchEvent(new Event("change", { bubbles: true }));
4482
+ });
4483
+
4429
4484
  console.log('EasyMDE initialized for field:', textarea.id || textarea.name);
4430
4485
  } catch (error) {
4431
4486
  console.error('Error initializing EasyMDE:', error);
@@ -12332,10 +12387,37 @@ function formatFileSize(bytes) {
12332
12387
  // src/templates/pages/admin-plugins-list.template.ts
12333
12388
  init_admin_layout_catalyst_template();
12334
12389
  function renderPluginsListPage(data) {
12390
+ const categories = [
12391
+ { value: "content", label: "Content Management" },
12392
+ { value: "media", label: "Media" },
12393
+ { value: "editor", label: "Editors" },
12394
+ { value: "seo", label: "SEO & Analytics" },
12395
+ { value: "security", label: "Security" },
12396
+ { value: "utilities", label: "Utilities" },
12397
+ { value: "system", label: "System" },
12398
+ { value: "development", label: "Development" },
12399
+ { value: "demo", label: "Demo" }
12400
+ ];
12401
+ const statuses = [
12402
+ { value: "active", label: "Active" },
12403
+ { value: "inactive", label: "Inactive" },
12404
+ { value: "uninstalled", label: "Available to Install" },
12405
+ { value: "error", label: "Error" }
12406
+ ];
12407
+ const categoryCounts = {};
12408
+ categories.forEach((cat) => {
12409
+ categoryCounts[cat.value] = data.plugins.filter((p) => p.category === cat.value).length;
12410
+ });
12411
+ categories.sort((a, b) => (categoryCounts[b.value] || 0) - (categoryCounts[a.value] || 0));
12412
+ const statusCounts = {};
12413
+ statuses.forEach((status) => {
12414
+ statusCounts[status.value] = data.plugins.filter((p) => p.status === status.value).length;
12415
+ });
12416
+ statuses.sort((a, b) => (statusCounts[b.value] || 0) - (statusCounts[a.value] || 0));
12335
12417
  const pageContent = `
12336
12418
  <div>
12337
12419
  <!-- Header -->
12338
- <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-6">
12420
+ <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-8">
12339
12421
  <div>
12340
12422
  <h1 class="text-2xl/8 font-semibold text-zinc-950 dark:text-white sm:text-xl/8">Plugins</h1>
12341
12423
  <p class="mt-2 text-sm/6 text-zinc-500 dark:text-zinc-400">Manage and extend functionality with plugins</p>
@@ -12343,7 +12425,7 @@ function renderPluginsListPage(data) {
12343
12425
  </div>
12344
12426
 
12345
12427
  <!-- Experimental Notice -->
12346
- <div class="mb-6 rounded-lg bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800/50 p-4">
12428
+ <div class="mb-8 rounded-lg bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800/50 p-4">
12347
12429
  <div class="flex items-start">
12348
12430
  <div class="flex-shrink-0">
12349
12431
  <svg class="h-5 w-5 text-amber-600 dark:text-amber-400" viewBox="0 0 20 20" fill="currentColor">
@@ -12364,176 +12446,174 @@ function renderPluginsListPage(data) {
12364
12446
  </div>
12365
12447
  </div>
12366
12448
 
12367
- <!-- Stats -->
12368
- <div class="mb-6">
12369
- <h3 class="text-base font-semibold text-zinc-950 dark:text-white">Plugin Statistics</h3>
12370
- <dl class="mt-5 grid grid-cols-1 divide-zinc-950/5 dark:divide-white/10 overflow-hidden rounded-lg bg-zinc-800/75 dark:bg-zinc-800/75 ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 md:grid-cols-5 md:divide-x md:divide-y-0">
12371
- <div class="px-4 py-5 sm:p-6">
12372
- <dt class="text-base font-normal text-zinc-700 dark:text-zinc-100">Total Plugins</dt>
12373
- <dd class="mt-1 flex items-baseline justify-between md:block lg:flex">
12374
- <div class="flex items-baseline text-2xl font-semibold text-cyan-400">
12375
- ${data.stats?.total || 0}
12376
- </div>
12377
- <div class="inline-flex items-baseline rounded-full bg-lime-400/10 text-lime-600 dark:text-lime-400 px-2.5 py-0.5 text-sm font-medium md:mt-2 lg:mt-0">
12378
- <svg viewBox="0 0 20 20" fill="currentColor" class="-ml-1 mr-0.5 size-5 shrink-0 self-center">
12379
- <path d="M10 17a.75.75 0 0 1-.75-.75V5.612L5.29 9.77a.75.75 0 0 1-1.08-1.04l5.25-5.5a.75.75 0 0 1 1.08 0l5.25 5.5a.75.75 0 1 1-1.08 1.04l-3.96-4.158V16.25A.75.75 0 0 1 10 17Z" clip-rule="evenodd" fill-rule="evenodd" />
12380
- </svg>
12381
- <span class="sr-only">Increased by</span>
12382
- 8.5%
12383
- </div>
12384
- </dd>
12385
- </div>
12386
- <div class="px-4 py-5 sm:p-6">
12387
- <dt class="text-base font-normal text-zinc-700 dark:text-zinc-100">Active Plugins</dt>
12388
- <dd class="mt-1 flex items-baseline justify-between md:block lg:flex">
12389
- <div class="flex items-baseline text-2xl font-semibold text-lime-400">
12390
- ${data.stats?.active || 0}
12391
- </div>
12392
- <div class="inline-flex items-baseline rounded-full bg-lime-400/10 text-lime-600 dark:text-lime-400 px-2.5 py-0.5 text-sm font-medium md:mt-2 lg:mt-0">
12393
- <svg viewBox="0 0 20 20" fill="currentColor" class="-ml-1 mr-0.5 size-5 shrink-0 self-center">
12394
- <path d="M10 17a.75.75 0 0 1-.75-.75V5.612L5.29 9.77a.75.75 0 0 1-1.08-1.04l5.25-5.5a.75.75 0 0 1 1.08 0l5.25 5.5a.75.75 0 1 1-1.08 1.04l-3.96-4.158V16.25A.75.75 0 0 1 10 17Z" clip-rule="evenodd" fill-rule="evenodd" />
12395
- </svg>
12396
- <span class="sr-only">Increased by</span>
12397
- 12.3%
12398
- </div>
12399
- </dd>
12449
+ <div class="flex flex-col lg:flex-row gap-8">
12450
+ <!-- Sidebar Filters -->
12451
+ <aside class="w-full lg:w-48 flex-shrink-0 space-y-8 lg:sticky lg:top-6 lg:self-start">
12452
+ <!-- Categories Filter -->
12453
+ <div>
12454
+ <h3 class="text-sm font-semibold text-zinc-950 dark:text-white mb-4">Categories</h3>
12455
+ <div class="space-y-3">
12456
+ ${categories.map((cat) => {
12457
+ const count = categoryCounts[cat.value] || 0;
12458
+ const isDisabled = count === 0;
12459
+ return `
12460
+ <div class="flex items-center ${isDisabled ? "opacity-50" : ""}">
12461
+ <input
12462
+ id="category-${cat.value}"
12463
+ name="category"
12464
+ value="${cat.value}"
12465
+ type="checkbox"
12466
+ onchange="filterAndSortPlugins()"
12467
+ class="h-4 w-4 rounded border-zinc-300 dark:border-zinc-700 text-zinc-900 focus:ring-zinc-600 dark:bg-zinc-900 disabled:cursor-not-allowed"
12468
+ ${isDisabled ? "disabled" : ""}
12469
+ >
12470
+ <label for="category-${cat.value}" class="ml-3 text-sm text-zinc-600 dark:text-zinc-400 select-none ${isDisabled ? "cursor-not-allowed" : ""}">
12471
+ ${cat.label} <span class="text-zinc-400 dark:text-zinc-500">(${count})</span>
12472
+ </label>
12473
+ </div>
12474
+ `;
12475
+ }).join("")}
12476
+ </div>
12400
12477
  </div>
12401
- <div class="px-4 py-5 sm:p-6">
12402
- <dt class="text-base font-normal text-zinc-700 dark:text-zinc-100">Inactive Plugins</dt>
12403
- <dd class="mt-1 flex items-baseline justify-between md:block lg:flex">
12404
- <div class="flex items-baseline text-2xl font-semibold text-purple-400">
12405
- ${data.stats?.inactive || 0}
12406
- </div>
12407
- <div class="inline-flex items-baseline rounded-full bg-pink-400/10 text-pink-600 dark:text-pink-400 px-2.5 py-0.5 text-sm font-medium md:mt-2 lg:mt-0">
12408
- <svg viewBox="0 0 20 20" fill="currentColor" class="-ml-1 mr-0.5 size-5 shrink-0 self-center">
12409
- <path d="M10 3a.75.75 0 0 1 .75.75v10.638l3.96-4.158a.75.75 0 1 1 1.08 1.04l-5.25 5.5a.75.75 0 0 1-1.08 0l-5.25-5.5a.75.75 0 1 1 1.08-1.04l3.96 4.158V3.75A.75.75 0 0 1 10 3Z" clip-rule="evenodd" fill-rule="evenodd" />
12410
- </svg>
12411
- <span class="sr-only">Decreased by</span>
12412
- 3.2%
12413
- </div>
12414
- </dd>
12478
+
12479
+ <div class="h-px bg-zinc-200 dark:bg-zinc-800 lg:hidden"></div>
12480
+
12481
+ <!-- Status Filter -->
12482
+ <div>
12483
+ <h3 class="text-sm font-semibold text-zinc-950 dark:text-white mb-4">Status</h3>
12484
+ <div class="space-y-3">
12485
+ ${statuses.map((status) => {
12486
+ const count = statusCounts[status.value] || 0;
12487
+ const isDisabled = count === 0;
12488
+ let colorClass = "";
12489
+ let ringClass = "";
12490
+ let dotClass = "";
12491
+ switch (status.value) {
12492
+ case "active":
12493
+ colorClass = "text-emerald-700 dark:text-emerald-400 bg-emerald-50 dark:bg-emerald-500/10";
12494
+ ringClass = "ring-emerald-600/20";
12495
+ dotClass = "bg-emerald-500 dark:bg-emerald-400";
12496
+ break;
12497
+ case "inactive":
12498
+ colorClass = "text-zinc-700 dark:text-zinc-400 bg-zinc-50 dark:bg-zinc-500/10";
12499
+ ringClass = "ring-zinc-600/20";
12500
+ dotClass = "bg-zinc-500 dark:bg-zinc-400";
12501
+ break;
12502
+ case "error":
12503
+ colorClass = "text-red-700 dark:text-red-400 bg-red-50 dark:bg-red-500/10";
12504
+ ringClass = "ring-red-600/20";
12505
+ dotClass = "bg-red-500 dark:bg-red-400";
12506
+ break;
12507
+ case "uninstalled":
12508
+ colorClass = "text-yellow-700 dark:text-yellow-400 bg-yellow-50 dark:bg-yellow-500/10";
12509
+ ringClass = "ring-yellow-600/20";
12510
+ dotClass = "bg-yellow-500 dark:bg-yellow-400";
12511
+ break;
12512
+ default:
12513
+ colorClass = "text-zinc-700 dark:text-zinc-400 bg-zinc-50 dark:bg-zinc-500/10";
12514
+ ringClass = "ring-zinc-600/20";
12515
+ dotClass = "bg-zinc-500 dark:bg-zinc-400";
12516
+ }
12517
+ return `
12518
+ <div class="flex items-center ${isDisabled ? "opacity-50" : ""}">
12519
+ <input
12520
+ id="status-${status.value}"
12521
+ name="status"
12522
+ value="${status.value}"
12523
+ type="checkbox"
12524
+ onchange="filterAndSortPlugins()"
12525
+ class="h-4 w-4 rounded border-zinc-300 dark:border-zinc-700 text-zinc-900 focus:ring-zinc-600 dark:bg-zinc-900 disabled:cursor-not-allowed"
12526
+ ${isDisabled ? "disabled" : ""}
12527
+ >
12528
+ <label for="status-${status.value}" class="ml-3 cursor-pointer select-none flex items-center ${isDisabled ? "cursor-not-allowed" : ""}">
12529
+ <span class="inline-flex items-center rounded-md px-2 py-1 text-xs font-medium ring-1 ring-inset ${colorClass} ${ringClass}">
12530
+ <span class="mr-1.5 h-1.5 w-1.5 rounded-full ${dotClass}"></span>
12531
+ ${status.label}
12532
+ </span>
12533
+ <span class="ml-2 text-xs text-zinc-500 dark:text-zinc-400">(${count})</span>
12534
+ </label>
12535
+ </div>
12536
+ `;
12537
+ }).join("")}
12538
+ </div>
12415
12539
  </div>
12416
- <div class="px-4 py-5 sm:p-6">
12417
- <dt class="text-base font-normal text-zinc-700 dark:text-zinc-100">Plugin Errors</dt>
12418
- <dd class="mt-1 flex items-baseline justify-between md:block lg:flex">
12419
- <div class="flex items-baseline text-2xl font-semibold text-pink-400">
12420
- ${data.stats?.errors || 0}
12421
- </div>
12422
- <div class="inline-flex items-baseline rounded-full bg-pink-400/10 text-pink-600 dark:text-pink-400 px-2.5 py-0.5 text-sm font-medium md:mt-2 lg:mt-0">
12423
- <svg viewBox="0 0 20 20" fill="currentColor" class="-ml-1 mr-0.5 size-5 shrink-0 self-center">
12424
- <path d="M10 3a.75.75 0 0 1 .75.75v10.638l3.96-4.158a.75.75 0 1 1 1.08 1.04l-5.25 5.5a.75.75 0 0 1-1.08 0l-5.25-5.5a.75.75 0 1 1 1.08-1.04l3.96 4.158V3.75A.75.75 0 0 1 10 3Z" clip-rule="evenodd" fill-rule="evenodd" />
12425
- </svg>
12426
- <span class="sr-only">Decreased by</span>
12427
- 1.5%
12428
- </div>
12429
- </dd>
12540
+ </aside>
12541
+
12542
+ <!-- Main Content -->
12543
+ <div class="flex-1 min-w-0">
12544
+ <!-- Stats Row (Compact) -->
12545
+ <div class="flex flex-wrap gap-4 mb-6">
12546
+ <div class="min-w-[140px] rounded-lg bg-zinc-50 dark:bg-zinc-800/50 p-3 ring-1 ring-inset ring-zinc-950/5 dark:ring-white/5">
12547
+ <div class="text-xs font-medium text-zinc-500 dark:text-zinc-400">Total</div>
12548
+ <div class="mt-1 text-lg font-semibold text-zinc-900 dark:text-white">${data.stats?.total || 0}</div>
12549
+ </div>
12550
+ <div class="min-w-[140px] rounded-lg bg-zinc-50 dark:bg-zinc-800/50 p-3 ring-1 ring-inset ring-zinc-950/5 dark:ring-white/5">
12551
+ <div class="text-xs font-medium text-zinc-500 dark:text-zinc-400">Active</div>
12552
+ <div class="mt-1 text-lg font-semibold text-emerald-600 dark:text-emerald-400">${data.stats?.active || 0}</div>
12553
+ </div>
12554
+ <div class="min-w-[140px] rounded-lg bg-zinc-50 dark:bg-zinc-800/50 p-3 ring-1 ring-inset ring-zinc-950/5 dark:ring-white/5">
12555
+ <div class="text-xs font-medium text-zinc-500 dark:text-zinc-400">Available</div>
12556
+ <div class="mt-1 text-lg font-semibold text-zinc-600 dark:text-zinc-400">${data.stats?.uninstalled || 0}</div>
12557
+ </div>
12558
+ <div class="min-w-[140px] rounded-lg bg-zinc-50 dark:bg-zinc-800/50 p-3 ring-1 ring-inset ring-zinc-950/5 dark:ring-white/5">
12559
+ <div class="text-xs font-medium text-zinc-500 dark:text-zinc-400">Errors</div>
12560
+ <div class="mt-1 text-lg font-semibold text-red-600 dark:text-red-400">${data.stats?.errors || 0}</div>
12561
+ </div>
12430
12562
  </div>
12431
- <div class="px-4 py-5 sm:p-6">
12432
- <dt class="text-base font-normal text-zinc-700 dark:text-zinc-100">Available to Install</dt>
12433
- <dd class="mt-1 flex items-baseline justify-between md:block lg:flex">
12434
- <div class="flex items-baseline text-2xl font-semibold text-zinc-400">
12435
- ${data.stats?.uninstalled || 0}
12436
- </div>
12437
- <div class="inline-flex items-baseline rounded-full bg-zinc-400/10 text-zinc-600 dark:text-zinc-400 px-2.5 py-0.5 text-sm font-medium md:mt-2 lg:mt-0">
12438
- <svg viewBox="0 0 20 20" fill="currentColor" class="-ml-1 mr-0.5 size-5 shrink-0 self-center">
12439
- <path d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z" />
12563
+
12564
+ <!-- Toolbar -->
12565
+ <div class="flex flex-col sm:flex-row gap-4 items-start sm:items-center justify-between mb-6">
12566
+ <div class="relative flex-1 w-full">
12567
+ <div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
12568
+ <svg class="h-4 w-4 text-zinc-400" viewBox="0 0 20 20" fill="currentColor">
12569
+ <path fill-rule="evenodd" d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z" clip-rule="evenodd" />
12440
12570
  </svg>
12441
- <span class="sr-only">Available</span>
12442
- Ready
12443
12571
  </div>
12444
- </dd>
12445
- </div>
12446
- </dl>
12447
- </div>
12572
+ <input
12573
+ id="search-input"
12574
+ type="text"
12575
+ placeholder="Search plugins..."
12576
+ oninput="filterAndSortPlugins()"
12577
+ class="block w-full h-9 rounded-md border-0 py-1.5 pl-10 text-zinc-900 ring-1 ring-inset ring-zinc-300 placeholder:text-zinc-400 focus:ring-2 focus:ring-inset focus:ring-zinc-600 dark:bg-zinc-900 dark:text-white dark:ring-zinc-700 dark:focus:ring-zinc-500 sm:text-sm sm:leading-6"
12578
+ >
12579
+ </div>
12448
12580
 
12449
- <!-- Filters -->
12450
- <div class="relative rounded-xl overflow-hidden mb-6">
12451
- <!-- Gradient Background -->
12452
- <div class="absolute inset-0 bg-gradient-to-r from-cyan-500/10 via-blue-500/10 to-purple-500/10 dark:from-cyan-400/20 dark:via-blue-400/20 dark:to-purple-400/20"></div>
12581
+ <div class="flex items-center gap-3 w-full sm:w-auto">
12582
+ <select id="sort-filter" onchange="filterAndSortPlugins()" class="block w-full sm:w-auto h-9 rounded-md border-0 py-1.5 pl-3 pr-8 text-zinc-900 ring-1 ring-inset ring-zinc-300 focus:ring-2 focus:ring-inset focus:ring-zinc-600 dark:bg-zinc-900 dark:text-white dark:ring-zinc-700 dark:focus:ring-zinc-500 sm:text-sm sm:leading-6">
12583
+ <option value="name-asc">Name (A-Z)</option>
12584
+ <option value="name-desc">Name (Z-A)</option>
12585
+ <option value="newest">Newest Installed</option>
12586
+ <option value="updated">Recently Updated</option>
12587
+ <option value="popular">Popularity</option>
12588
+ <option value="rating">Highest Rated</option>
12589
+ </select>
12453
12590
 
12454
- <div class="relative bg-white/80 dark:bg-zinc-900/80 backdrop-blur-xl shadow-sm ring-1 ring-zinc-950/5 dark:ring-white/10">
12455
- <div class="px-6 py-5">
12456
- <div class="flex items-center justify-between">
12457
- <div class="flex items-center space-x-4 flex-1">
12458
- <div>
12459
- <label class="block text-sm/6 font-medium text-zinc-950 dark:text-white">Category</label>
12460
- <div class="mt-2 grid grid-cols-1">
12461
- <select id="category-filter" onchange="filterPlugins()" class="col-start-1 row-start-1 w-full appearance-none rounded-md bg-white/5 dark:bg-white/5 py-1.5 pl-3 pr-8 text-base text-zinc-950 dark:text-white outline outline-1 -outline-offset-1 outline-cyan-500/30 dark:outline-cyan-400/30 *:bg-white dark:*:bg-zinc-800 focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-cyan-500 dark:focus-visible:outline-cyan-400 sm:text-sm/6 min-w-48">
12462
- <option value="">All Categories</option>
12463
- <option value="content">Content Management</option>
12464
- <option value="media">Media</option>
12465
- <option value="seo">SEO & Analytics</option>
12466
- <option value="security">Security</option>
12467
- <option value="utilities">Utilities</option>
12468
- <option value="system">System</option>
12469
- <option value="development">Development</option>
12470
- <option value="demo">Demo</option>
12471
- </select>
12472
- <svg viewBox="0 0 16 16" fill="currentColor" data-slot="icon" aria-hidden="true" class="pointer-events-none col-start-1 row-start-1 mr-2 size-5 self-center justify-self-end text-cyan-600 dark:text-cyan-400 sm:size-4">
12473
- <path d="M4.22 6.22a.75.75 0 0 1 1.06 0L8 8.94l2.72-2.72a.75.75 0 1 1 1.06 1.06l-3.25 3.25a.75.75 0 0 1-1.06 0L4.22 7.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" fill-rule="evenodd" />
12474
- </svg>
12475
- </div>
12476
- </div>
12477
- <div>
12478
- <label class="block text-sm/6 font-medium text-zinc-950 dark:text-white">Status</label>
12479
- <div class="mt-2 grid grid-cols-1">
12480
- <select id="status-filter" onchange="filterPlugins()" class="col-start-1 row-start-1 w-full appearance-none rounded-md bg-white/5 dark:bg-white/5 py-1.5 pl-3 pr-8 text-base text-zinc-950 dark:text-white outline outline-1 -outline-offset-1 outline-cyan-500/30 dark:outline-cyan-400/30 *:bg-white dark:*:bg-zinc-800 focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-cyan-500 dark:focus-visible:outline-cyan-400 sm:text-sm/6 min-w-48">
12481
- <option value="">All Status</option>
12482
- <option value="active">Active</option>
12483
- <option value="inactive">Inactive</option>
12484
- <option value="uninstalled">Available to Install</option>
12485
- <option value="error">Error</option>
12486
- </select>
12487
- <svg viewBox="0 0 16 16" fill="currentColor" data-slot="icon" aria-hidden="true" class="pointer-events-none col-start-1 row-start-1 mr-2 size-5 self-center justify-self-end text-cyan-600 dark:text-cyan-400 sm:size-4">
12488
- <path d="M4.22 6.22a.75.75 0 0 1 1.06 0L8 8.94l2.72-2.72a.75.75 0 1 1 1.06 1.06l-3.25 3.25a.75.75 0 0 1-1.06 0L4.22 7.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" fill-rule="evenodd" />
12489
- </svg>
12490
- </div>
12491
- </div>
12492
- <div class="flex-1 max-w-md">
12493
- <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Search</label>
12494
- <div class="relative group">
12495
- <div class="absolute left-3.5 top-2.5 flex items-center justify-center w-5 h-5 rounded-full bg-gradient-to-br from-cyan-400 to-blue-500 dark:from-cyan-300 dark:to-blue-400 opacity-90 group-focus-within:opacity-100 transition-opacity">
12496
- <svg class="h-3 w-3 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
12497
- <path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
12498
- </svg>
12499
- </div>
12500
- <input
12501
- id="search-input"
12502
- type="text"
12503
- placeholder="Search plugins..."
12504
- oninput="filterPlugins()"
12505
- class="w-full rounded-full bg-transparent px-11 py-2 text-sm text-zinc-950 dark:text-white placeholder-zinc-500 dark:placeholder-zinc-400 border-2 border-cyan-200/50 dark:border-cyan-700/50 focus:outline-none focus:border-cyan-500 dark:focus:border-cyan-400 focus:shadow-lg focus:shadow-cyan-500/20 dark:focus:shadow-cyan-400/20 transition-all duration-300"
12506
- />
12507
- </div>
12508
- </div>
12509
- </div>
12510
- <div class="flex items-center gap-x-3 ml-4">
12511
- <button
12512
- onclick="location.reload()"
12513
- class="inline-flex items-center gap-x-1.5 px-3 py-1.5 bg-white/90 dark:bg-zinc-800/90 backdrop-blur-sm text-zinc-950 dark:text-white text-sm font-medium rounded-full ring-1 ring-inset ring-cyan-200/50 dark:ring-cyan-700/50 hover:bg-gradient-to-r hover:from-cyan-50 hover:to-blue-50 dark:hover:from-cyan-900/30 dark:hover:to-blue-900/30 hover:ring-cyan-300 dark:hover:ring-cyan-600 transition-all duration-200"
12514
- >
12515
- <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
12516
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
12517
- </svg>
12518
- Refresh
12519
- </button>
12520
- </div>
12591
+ <button
12592
+ onclick="location.reload()"
12593
+ class="inline-flex items-center gap-x-1.5 rounded-md bg-white dark:bg-zinc-900 px-3 py-1.5 h-9 text-sm font-semibold text-zinc-900 dark:text-white shadow-sm ring-1 ring-inset ring-zinc-300 dark:ring-zinc-700 hover:bg-zinc-50 dark:hover:bg-zinc-800"
12594
+ >
12595
+ <svg class="h-4 w-4 text-zinc-500 dark:text-zinc-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
12596
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
12597
+ </svg>
12598
+ </button>
12521
12599
  </div>
12522
12600
  </div>
12601
+
12602
+ <!-- Plugins Grid -->
12603
+ <div id="plugins-grid" class="grid gap-6" style="grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));">
12604
+ ${data.plugins.map((plugin) => renderPluginCard(plugin)).join("")}
12605
+ </div>
12523
12606
  </div>
12524
12607
  </div>
12525
-
12526
- <!-- Plugins Grid -->
12527
- <div id="plugins-grid" class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
12528
- ${data.plugins.map((plugin) => renderPluginCard(plugin)).join("")}
12529
12608
  </div>
12530
12609
 
12531
12610
  <script>
12532
- async function togglePlugin(pluginId, action) {
12533
- const button = event.target;
12534
- const originalText = button.textContent;
12611
+ async function togglePlugin(pluginId, action, event) {
12612
+ const button = event.target.closest('button');
12613
+ if (!button) return;
12614
+
12535
12615
  button.disabled = true;
12536
- button.textContent = action === 'activate' ? 'Activating...' : 'Deactivating...';
12616
+ button.classList.add('opacity-50', 'cursor-wait');
12537
12617
 
12538
12618
  try {
12539
12619
  const response = await fetch(\`/admin/plugins/\${pluginId}/\${action}\`, {
@@ -12549,27 +12629,36 @@ function renderPluginsListPage(data) {
12549
12629
  // Update UI
12550
12630
  const card = button.closest('.plugin-card');
12551
12631
  const statusBadge = card.querySelector('.status-badge');
12632
+ const knob = button.querySelector('.toggle-knob');
12552
12633
 
12553
12634
  if (action === 'activate') {
12554
12635
  // Update status badge
12555
- statusBadge.className = 'status-badge inline-flex items-center rounded-md px-2.5 py-1 text-sm font-medium ring-1 ring-inset bg-lime-50 dark:bg-lime-500/10 text-lime-700 dark:text-lime-300 ring-lime-700/10 dark:ring-lime-400/20';
12556
- statusBadge.innerHTML = '<div class="w-2 h-2 bg-lime-500 dark:bg-lime-400 rounded-full mr-2"></div>Active';
12557
- // Update card border to green
12558
- card.className = 'plugin-card rounded-xl bg-white dark:bg-zinc-900 shadow-sm ring-[3px] ring-lime-500 dark:ring-lime-400 p-6 hover:bg-zinc-50 dark:hover:bg-zinc-800/50 transition-all';
12559
- // Update button
12560
- button.textContent = 'Deactivate';
12561
- button.onclick = () => togglePlugin(pluginId, 'deactivate');
12562
- button.className = 'bg-red-600 dark:bg-red-700 hover:bg-red-700 dark:hover:bg-red-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-colors';
12636
+ statusBadge.className = 'status-badge inline-flex items-center rounded-full px-1.5 py-0.5 text-[10px] font-medium ring-1 ring-inset bg-emerald-50 dark:bg-emerald-500/10 text-emerald-700 dark:text-emerald-400 ring-emerald-600/20';
12637
+ statusBadge.innerHTML = '<div class="w-1.5 h-1.5 bg-emerald-500 dark:bg-emerald-400 rounded-full mr-1.5"></div>Active';
12638
+
12639
+ // Update button state to Active
12640
+ button.className = 'bg-emerald-600 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-emerald-600 focus:ring-offset-2 toggle-button';
12641
+ button.setAttribute('aria-checked', 'true');
12642
+ button.onclick = (event) => togglePlugin(pluginId, 'deactivate', event);
12643
+
12644
+ // Update knob position
12645
+ if (knob) {
12646
+ knob.className = 'translate-x-5 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out toggle-knob';
12647
+ }
12563
12648
  } else {
12564
12649
  // Update status badge
12565
- statusBadge.className = 'status-badge inline-flex items-center rounded-md px-2.5 py-1 text-sm font-medium ring-1 ring-inset bg-zinc-50 dark:bg-zinc-500/10 text-zinc-700 dark:text-zinc-400 ring-zinc-700/10 dark:ring-zinc-400/20';
12566
- statusBadge.innerHTML = '<div class="w-2 h-2 bg-zinc-500 dark:bg-zinc-400 rounded-full mr-2"></div>Inactive';
12567
- // Update card border to pink
12568
- card.className = 'plugin-card rounded-xl bg-white dark:bg-zinc-900 shadow-sm ring-[3px] ring-pink-500 dark:ring-pink-400 p-6 hover:bg-zinc-50 dark:hover:bg-zinc-800/50 transition-all';
12569
- // Update button
12570
- button.textContent = 'Activate';
12571
- button.onclick = () => togglePlugin(pluginId, 'activate');
12572
- button.className = 'bg-lime-600 dark:bg-lime-700 hover:bg-lime-700 dark:hover:bg-lime-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-colors';
12650
+ statusBadge.className = 'status-badge inline-flex items-center rounded-full px-1.5 py-0.5 text-[10px] font-medium ring-1 ring-inset bg-zinc-50 dark:bg-zinc-500/10 text-zinc-700 dark:text-zinc-400 ring-zinc-600/20';
12651
+ statusBadge.innerHTML = '<div class="w-1.5 h-1.5 bg-zinc-500 dark:bg-zinc-400 rounded-full mr-1.5"></div>Inactive';
12652
+
12653
+ // Update button state to Inactive
12654
+ button.className = 'bg-zinc-200 dark:bg-zinc-700 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-emerald-600 focus:ring-offset-2 toggle-button';
12655
+ button.setAttribute('aria-checked', 'false');
12656
+ button.onclick = (event) => togglePlugin(pluginId, 'activate', event);
12657
+
12658
+ // Update knob position
12659
+ if (knob) {
12660
+ knob.className = 'translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out toggle-knob';
12661
+ }
12573
12662
  }
12574
12663
 
12575
12664
  showNotification(\`Plugin \${action}d successfully\`, 'success');
@@ -12578,9 +12667,9 @@ function renderPluginsListPage(data) {
12578
12667
  }
12579
12668
  } catch (error) {
12580
12669
  showNotification(error.message, 'error');
12581
- button.textContent = originalText;
12582
12670
  } finally {
12583
12671
  button.disabled = false;
12672
+ button.classList.remove('opacity-50', 'cursor-wait');
12584
12673
  }
12585
12674
  }
12586
12675
 
@@ -12664,81 +12753,92 @@ function renderPluginsListPage(data) {
12664
12753
  showNotification('Plugin details coming soon!', 'info');
12665
12754
  }
12666
12755
 
12667
- function showNotification(message, type) {
12668
- const notification = document.createElement('div');
12669
- const bgColor = type === 'success' ? 'bg-green-600' : type === 'error' ? 'bg-red-600' : 'bg-blue-600';
12670
- notification.className = \`fixed top-4 right-4 px-4 py-2 rounded-lg text-white z-50 \${bgColor}\`;
12671
- notification.textContent = message;
12672
- document.body.appendChild(notification);
12673
-
12674
- setTimeout(() => {
12675
- notification.remove();
12676
- }, 3000);
12677
- }
12678
-
12679
- function filterPlugins() {
12680
- const categoryFilter = document.getElementById('category-filter').value.toLowerCase();
12681
- const statusFilter = document.getElementById('status-filter').value.toLowerCase();
12756
+ function filterAndSortPlugins() {
12757
+ // Get checked categories
12758
+ const checkedCategories = Array.from(document.querySelectorAll('input[name="category"]:checked'))
12759
+ .map(cb => cb.value.toLowerCase());
12760
+
12761
+ // Get checked statuses
12762
+ const checkedStatuses = Array.from(document.querySelectorAll('input[name="status"]:checked'))
12763
+ .map(cb => cb.value.toLowerCase());
12764
+
12682
12765
  const searchInput = document.getElementById('search-input').value.toLowerCase();
12766
+ const sortValue = document.getElementById('sort-filter').value;
12683
12767
 
12684
- const pluginCards = document.querySelectorAll('.plugin-card');
12685
- let visibleCount = 0;
12686
-
12687
- pluginCards.forEach(card => {
12688
- // Get plugin data from card attributes
12768
+ const pluginsGrid = document.getElementById('plugins-grid');
12769
+ const pluginCards = Array.from(pluginsGrid.querySelectorAll('.plugin-card'));
12770
+
12771
+ // Filter
12772
+ const visibleCards = pluginCards.filter(card => {
12689
12773
  const category = card.getAttribute('data-category')?.toLowerCase() || '';
12690
12774
  const status = card.getAttribute('data-status')?.toLowerCase() || '';
12691
12775
  const name = card.getAttribute('data-name')?.toLowerCase() || '';
12692
12776
  const description = card.getAttribute('data-description')?.toLowerCase() || '';
12693
12777
 
12694
- // Check if plugin matches all filters
12695
- let matches = true;
12696
-
12697
- // Category filter
12698
- if (categoryFilter && category !== categoryFilter) {
12699
- matches = false;
12700
- }
12701
-
12702
- // Status filter
12703
- if (statusFilter && status !== statusFilter) {
12704
- matches = false;
12705
- }
12706
-
12707
- // Search filter - check if search term is in name or description
12708
- if (searchInput && !name.includes(searchInput) && !description.includes(searchInput)) {
12709
- matches = false;
12710
- }
12778
+ // Category filter: if any selected, must match one of them
12779
+ if (checkedCategories.length > 0 && !checkedCategories.includes(category)) return false;
12780
+
12781
+ // Status filter: if any selected, must match one of them
12782
+ if (checkedStatuses.length > 0 && !checkedStatuses.includes(status)) return false;
12783
+
12784
+ // Search filter
12785
+ if (searchInput && !name.includes(searchInput) && !description.includes(searchInput)) return false;
12786
+
12787
+ return true;
12788
+ });
12711
12789
 
12712
- // Show/hide card
12713
- if (matches) {
12714
- card.style.display = '';
12715
- visibleCount++;
12716
- } else {
12717
- card.style.display = 'none';
12790
+ // Sort
12791
+ visibleCards.sort((a, b) => {
12792
+ const aName = a.getAttribute('data-name') || '';
12793
+ const bName = b.getAttribute('data-name') || '';
12794
+ const aInstalled = parseInt(a.getAttribute('data-installed') || '0');
12795
+ const bInstalled = parseInt(b.getAttribute('data-installed') || '0');
12796
+ const aUpdated = parseInt(a.getAttribute('data-updated') || '0');
12797
+ const bUpdated = parseInt(b.getAttribute('data-updated') || '0');
12798
+ const aDownloads = parseInt(a.getAttribute('data-downloads') || '0');
12799
+ const bDownloads = parseInt(b.getAttribute('data-downloads') || '0');
12800
+ const aRating = parseFloat(a.getAttribute('data-rating') || '0');
12801
+ const bRating = parseFloat(b.getAttribute('data-rating') || '0');
12802
+
12803
+ switch (sortValue) {
12804
+ case 'name-desc': return bName.localeCompare(aName);
12805
+ case 'newest': return bInstalled - aInstalled;
12806
+ case 'updated': return bUpdated - aUpdated;
12807
+ case 'popular': return bDownloads - aDownloads;
12808
+ case 'rating': return bRating - aRating;
12809
+ case 'name-asc':
12810
+ default: return aName.localeCompare(bName);
12718
12811
  }
12719
12812
  });
12720
12813
 
12721
- // Show/hide "no results" message
12814
+ // Re-append
12815
+ pluginCards.forEach(card => card.style.display = 'none'); // Hide all first
12816
+
12817
+ // If no results
12722
12818
  let noResultsMsg = document.getElementById('no-results-message');
12723
- if (visibleCount === 0) {
12819
+ if (visibleCards.length === 0) {
12724
12820
  if (!noResultsMsg) {
12725
12821
  noResultsMsg = document.createElement('div');
12726
12822
  noResultsMsg.id = 'no-results-message';
12727
- noResultsMsg.className = 'col-span-full text-center py-12';
12823
+ noResultsMsg.className = 'col-span-full text-center py-12 bg-zinc-50 dark:bg-zinc-800/50 rounded-lg border border-dashed border-zinc-300 dark:border-zinc-700';
12728
12824
  noResultsMsg.innerHTML = \`
12729
12825
  <div class="flex flex-col items-center">
12730
- <svg class="w-16 h-16 text-zinc-400 dark:text-zinc-600 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
12826
+ <svg class="w-12 h-12 text-zinc-400 dark:text-zinc-600 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
12731
12827
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
12732
12828
  </svg>
12733
- <h3 class="text-lg font-semibold text-zinc-950 dark:text-white mb-2">No plugins found</h3>
12829
+ <h3 class="text-base font-semibold text-zinc-950 dark:text-white mb-1">No plugins found</h3>
12734
12830
  <p class="text-sm text-zinc-500 dark:text-zinc-400">Try adjusting your filters or search terms</p>
12735
12831
  </div>
12736
12832
  \`;
12737
- document.getElementById('plugins-grid').appendChild(noResultsMsg);
12833
+ pluginsGrid.appendChild(noResultsMsg);
12738
12834
  }
12739
12835
  noResultsMsg.style.display = '';
12740
- } else if (noResultsMsg) {
12741
- noResultsMsg.style.display = 'none';
12836
+ } else {
12837
+ if (noResultsMsg) noResultsMsg.style.display = 'none';
12838
+ visibleCards.forEach(card => {
12839
+ card.style.display = '';
12840
+ pluginsGrid.appendChild(card); // Re-appending moves it to the end, effectively sorting
12841
+ });
12742
12842
  }
12743
12843
  }
12744
12844
  </script>
@@ -12769,116 +12869,111 @@ function renderPluginsListPage(data) {
12769
12869
  }
12770
12870
  function renderPluginCard(plugin) {
12771
12871
  const statusColors = {
12772
- active: "bg-lime-50 dark:bg-lime-500/10 text-lime-700 dark:text-lime-300 ring-lime-700/10 dark:ring-lime-400/20",
12773
- inactive: "bg-zinc-50 dark:bg-zinc-500/10 text-zinc-700 dark:text-zinc-400 ring-zinc-700/10 dark:ring-zinc-400/20",
12774
- error: "bg-red-50 dark:bg-red-500/10 text-red-700 dark:text-red-400 ring-red-700/10 dark:ring-red-400/20",
12775
- uninstalled: "bg-zinc-100 dark:bg-zinc-600/10 text-zinc-600 dark:text-zinc-500 ring-zinc-600/10 dark:ring-zinc-500/20"
12872
+ active: "bg-emerald-50 dark:bg-emerald-500/10 text-emerald-700 dark:text-emerald-400 ring-emerald-600/20",
12873
+ inactive: "bg-zinc-50 dark:bg-zinc-500/10 text-zinc-700 dark:text-zinc-400 ring-zinc-600/20",
12874
+ error: "bg-red-50 dark:bg-red-500/10 text-red-700 dark:text-red-400 ring-red-600/20",
12875
+ uninstalled: "bg-zinc-50 dark:bg-zinc-500/10 text-zinc-600 dark:text-zinc-500 ring-zinc-600/20"
12776
12876
  };
12777
12877
  const statusIcons = {
12778
- active: '<div class="w-2 h-2 bg-lime-500 dark:bg-lime-400 rounded-full mr-2"></div>',
12779
- inactive: '<div class="w-2 h-2 bg-zinc-500 dark:bg-zinc-400 rounded-full mr-2"></div>',
12780
- error: '<div class="w-2 h-2 bg-red-500 dark:bg-red-400 rounded-full mr-2"></div>',
12781
- uninstalled: '<div class="w-2 h-2 bg-zinc-400 dark:bg-zinc-600 rounded-full mr-2"></div>'
12782
- };
12783
- const borderColors = {
12784
- active: "ring-[3px] ring-lime-500 dark:ring-lime-400",
12785
- inactive: "ring-[3px] ring-pink-500 dark:ring-pink-400",
12786
- error: "ring-[3px] ring-red-500 dark:ring-red-400",
12787
- uninstalled: "ring-[3px] ring-zinc-400 dark:ring-zinc-600"
12878
+ active: '<div class="w-1.5 h-1.5 bg-emerald-500 dark:bg-emerald-400 rounded-full mr-1.5"></div>',
12879
+ inactive: '<div class="w-1.5 h-1.5 bg-zinc-500 dark:bg-zinc-400 rounded-full mr-1.5"></div>',
12880
+ error: '<div class="w-1.5 h-1.5 bg-red-500 dark:bg-red-400 rounded-full mr-1.5"></div>',
12881
+ uninstalled: '<div class="w-1.5 h-1.5 bg-zinc-400 dark:bg-zinc-600 rounded-full mr-1.5"></div>'
12788
12882
  };
12789
12883
  const criticalCorePlugins = ["core-auth", "core-media"];
12790
12884
  const canToggle = !criticalCorePlugins.includes(plugin.id);
12791
12885
  let actionButton = "";
12792
12886
  if (plugin.status === "uninstalled") {
12793
- actionButton = `<button onclick="installPlugin('${plugin.name}')" class="bg-cyan-600 dark:bg-cyan-700 hover:bg-cyan-700 dark:hover:bg-cyan-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-colors">Install</button>`;
12794
- } else if (plugin.status === "active") {
12795
- actionButton = `<button onclick="togglePlugin('${plugin.id}', 'deactivate')" class="bg-red-600 dark:bg-red-700 hover:bg-red-700 dark:hover:bg-red-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-colors">Deactivate</button>`;
12887
+ actionButton = `<button onclick="installPlugin('${plugin.name}')" class="w-full sm:w-auto bg-zinc-900 dark:bg-white hover:bg-zinc-800 dark:hover:bg-zinc-100 text-white dark:text-zinc-900 px-3 py-1.5 rounded-md text-xs font-medium transition-colors shadow-sm">Install</button>`;
12796
12888
  } else {
12797
- actionButton = `<button onclick="togglePlugin('${plugin.id}', 'activate')" class="bg-lime-600 dark:bg-lime-700 hover:bg-lime-700 dark:hover:bg-lime-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-colors">Activate</button>`;
12889
+ const isActive = plugin.status === "active";
12890
+ const action = isActive ? "deactivate" : "activate";
12891
+ const bgClass = isActive ? "bg-emerald-600" : "bg-zinc-200 dark:bg-zinc-700";
12892
+ const translateClass = isActive ? "translate-x-5" : "translate-x-0";
12893
+ if (canToggle) {
12894
+ actionButton = `
12895
+ <button onclick="togglePlugin('${plugin.id}', '${action}', event)" type="button" class="${bgClass} relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-emerald-600 focus:ring-offset-2 toggle-button" role="switch" aria-checked="${isActive}">
12896
+ <span class="sr-only">Toggle plugin</span>
12897
+ <span aria-hidden="true" class="${translateClass} pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out toggle-knob"></span>
12898
+ </button>
12899
+ `;
12900
+ } else {
12901
+ actionButton = `
12902
+ <div class="relative inline-flex h-6 w-11 flex-shrink-0 cursor-not-allowed rounded-full border-2 border-transparent bg-emerald-600/50 opacity-50" title="Core plugin cannot be disabled">
12903
+ <span class="translate-x-5 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0"></span>
12904
+ </div>
12905
+ `;
12906
+ }
12798
12907
  }
12799
12908
  return `
12800
- <div class="plugin-card rounded-xl bg-white dark:bg-zinc-900 shadow-sm ${borderColors[plugin.status]} p-6 hover:bg-zinc-50 dark:hover:bg-zinc-800/50 transition-all" data-category="${plugin.category}" data-status="${plugin.status}" data-name="${plugin.displayName}" data-description="${plugin.description}">
12909
+ <div class="plugin-card flex flex-col h-full rounded-md bg-white dark:bg-zinc-900 ring-1 ring-zinc-950/10 dark:ring-white/10 p-5 transition-all hover:shadow-md"
12910
+ data-category="${plugin.category}"
12911
+ data-status="${plugin.status}"
12912
+ data-name="${plugin.displayName}"
12913
+ data-description="${plugin.description}"
12914
+ data-downloads="${plugin.downloadCount || 0}"
12915
+ data-rating="${plugin.rating || 0}">
12801
12916
  <div class="flex items-start justify-between mb-4">
12802
12917
  <div class="flex items-center gap-3">
12803
- <div class="w-12 h-12 rounded-lg flex items-center justify-center ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 bg-zinc-50 dark:bg-zinc-800">
12918
+ <div class="w-10 h-10 rounded-md flex items-center justify-center bg-zinc-50 dark:bg-zinc-800 text-zinc-600 dark:text-zinc-400 ring-1 ring-inset ring-zinc-200 dark:ring-zinc-700/50">
12804
12919
  ${plugin.icon || getDefaultPluginIcon(plugin.category)}
12805
12920
  </div>
12806
12921
  <div>
12807
- <h3 class="text-lg font-semibold text-zinc-950 dark:text-white">${plugin.displayName}</h3>
12808
- <p class="text-sm text-zinc-500 dark:text-zinc-400">v${plugin.version} by ${plugin.author}</p>
12922
+ <div class="flex items-center gap-2">
12923
+ <h3 class="text-sm font-semibold text-zinc-900 dark:text-white">${plugin.displayName}</h3>
12924
+ <span class="status-badge inline-flex items-center rounded-full px-1.5 py-0.5 text-[10px] font-medium ring-1 ring-inset ${statusColors[plugin.status]}">
12925
+ ${statusIcons[plugin.status]}${plugin.status.charAt(0).toUpperCase() + plugin.status.slice(1)}
12926
+ </span>
12927
+ </div>
12928
+ <p class="text-xs text-zinc-500 dark:text-zinc-400">v${plugin.version} \u2022 ${plugin.author}</p>
12809
12929
  </div>
12810
12930
  </div>
12811
- <div class="flex flex-col items-end gap-2">
12812
- <span class="status-badge inline-flex items-center rounded-md px-2.5 py-1 text-sm font-medium ring-1 ring-inset ${statusColors[plugin.status]}">
12813
- ${statusIcons[plugin.status]}${plugin.status.charAt(0).toUpperCase() + plugin.status.slice(1)}
12814
- </span>
12815
- ${plugin.isCore ? '<span class="inline-flex items-center rounded-md px-2.5 py-1 text-sm font-medium bg-cyan-50 dark:bg-cyan-500/10 text-cyan-700 dark:text-cyan-300 ring-1 ring-inset ring-cyan-700/10 dark:ring-cyan-400/20">Core</span>' : ""}
12931
+
12932
+ <div class="flex items-center gap-1">
12933
+ ${plugin.status !== "uninstalled" ? `
12934
+ <button onclick="showPluginDetails('${plugin.id}')" class="text-zinc-400 hover:text-zinc-600 dark:text-zinc-500 dark:hover:text-zinc-300 p-1.5 rounded-md hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors" title="Plugin Details">
12935
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
12936
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
12937
+ </svg>
12938
+ </button>
12939
+ ` : ""}
12940
+
12941
+ ${!plugin.isCore && plugin.status !== "uninstalled" ? `
12942
+ <button onclick="uninstallPlugin('${plugin.id}')" class="text-zinc-400 hover:text-red-600 dark:text-zinc-500 dark:hover:text-red-400 p-1.5 rounded-md hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors" title="Uninstall Plugin">
12943
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
12944
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
12945
+ </svg>
12946
+ </button>
12947
+ ` : ""}
12816
12948
  </div>
12817
12949
  </div>
12818
12950
 
12819
- <p class="text-zinc-600 dark:text-zinc-300 text-sm mb-4 line-clamp-3">${plugin.description}</p>
12951
+ <p class="text-zinc-600 dark:text-zinc-400 text-sm mb-4 line-clamp-2 flex-grow">${plugin.description}</p>
12820
12952
 
12821
- <div class="flex items-center gap-4 mb-4 text-xs text-zinc-500 dark:text-zinc-400">
12822
- <span class="flex items-center gap-1">
12823
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
12824
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"/>
12825
- </svg>
12953
+ <div class="flex flex-wrap items-center gap-2 mb-5">
12954
+ <span class="inline-flex items-center rounded-md bg-zinc-100 dark:bg-zinc-800 px-2 py-1 text-xs font-medium text-zinc-600 dark:text-zinc-400">
12826
12955
  ${plugin.category}
12827
12956
  </span>
12828
-
12829
- ${plugin.downloadCount ? `
12830
- <span class="flex items-center gap-1">
12831
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
12832
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"/>
12833
- </svg>
12834
- ${plugin.downloadCount.toLocaleString()}
12835
- </span>
12836
- ` : ""}
12837
-
12838
- ${plugin.rating ? `
12839
- <span class="flex items-center gap-1">
12840
- <svg class="w-4 h-4 text-yellow-500 dark:text-yellow-400 fill-current" viewBox="0 0 24 24">
12841
- <path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
12842
- </svg>
12843
- ${plugin.rating}
12844
- </span>
12845
- ` : ""}
12846
-
12847
- <span>${plugin.lastUpdated}</span>
12848
- </div>
12849
-
12850
- ${plugin.dependencies && plugin.dependencies.length > 0 ? `
12851
- <div class="mb-4">
12852
- <p class="text-xs text-zinc-500 dark:text-zinc-400 mb-2">Dependencies:</p>
12853
- <div class="flex flex-wrap gap-1">
12854
- ${plugin.dependencies.map((dep) => `<span class="inline-block bg-zinc-100 dark:bg-zinc-800 text-zinc-700 dark:text-zinc-300 text-xs px-2 py-1 rounded">${dep}</span>`).join("")}
12855
- </div>
12957
+ ${plugin.isCore ? '<span class="inline-flex items-center rounded-md bg-zinc-100 dark:bg-zinc-800 px-2 py-1 text-xs font-medium text-zinc-600 dark:text-zinc-400">Core</span>' : ""}
12958
+
12959
+ ${plugin.dependencies && plugin.dependencies.map((dep) => `
12960
+ <span class="inline-flex items-center rounded-md bg-zinc-100 dark:bg-zinc-800 px-2 py-1 text-xs font-medium text-zinc-600 dark:text-zinc-400">
12961
+ ${dep}
12962
+ </span>
12963
+ `).join("") || ""}
12856
12964
  </div>
12857
- ` : ""}
12858
12965
 
12859
- <div class="flex items-center justify-between">
12966
+ <div class="flex items-center justify-between pt-4 border-t border-zinc-100 dark:border-zinc-800 mt-auto">
12860
12967
  <div class="flex gap-2">
12861
- ${plugin.status === "uninstalled" ? actionButton : canToggle ? actionButton : ""}
12862
- ${plugin.status !== "uninstalled" ? `
12863
- <button onclick="openPluginSettings('${plugin.id}')" class="bg-white dark:bg-zinc-800 hover:bg-zinc-50 dark:hover:bg-zinc-700 text-zinc-950 dark:text-white ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 px-3 py-1.5 rounded-lg text-sm font-medium transition-colors">
12864
- Settings
12865
- </button>
12866
- ` : ""}
12968
+ ${actionButton}
12867
12969
  </div>
12868
12970
 
12869
12971
  <div class="flex items-center gap-2">
12870
12972
  ${plugin.status !== "uninstalled" ? `
12871
- <button onclick="showPluginDetails('${plugin.id}')" class="text-zinc-500 dark:text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-300 p-1.5 rounded-lg hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors" title="Plugin Details">
12872
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
12873
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
12874
- </svg>
12875
- </button>
12876
- ` : ""}
12877
-
12878
- ${!plugin.isCore && plugin.status !== "uninstalled" ? `
12879
- <button onclick="uninstallPlugin('${plugin.id}')" class="text-zinc-500 dark:text-zinc-400 hover:text-red-600 dark:hover:text-red-400 p-1.5 rounded-lg hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors" title="Uninstall Plugin">
12880
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
12881
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
12973
+ <button onclick="openPluginSettings('${plugin.id}')" class="text-zinc-400 hover:text-zinc-600 dark:text-zinc-500 dark:hover:text-zinc-300 p-1.5 rounded-md hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors" title="Settings">
12974
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
12975
+ <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"/>
12976
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
12882
12977
  </svg>
12883
12978
  </button>
12884
12979
  ` : ""}
@@ -21662,5 +21757,5 @@ var ROUTES_INFO = {
21662
21757
  };
21663
21758
 
21664
21759
  export { PluginBuilder, ROUTES_INFO, adminCheckboxRoutes, adminCollectionsRoutes, adminDesignRoutes, adminLogsRoutes, adminMediaRoutes, adminPluginRoutes, adminSettingsRoutes, admin_api_default, admin_code_examples_default, admin_content_default, admin_testimonials_default, api_content_crud_default, api_default, api_media_default, api_system_default, auth_default, router, test_cleanup_default, userRoutes };
21665
- //# sourceMappingURL=chunk-RP66TPEJ.js.map
21666
- //# sourceMappingURL=chunk-RP66TPEJ.js.map
21760
+ //# sourceMappingURL=chunk-4M2UOUXV.js.map
21761
+ //# sourceMappingURL=chunk-4M2UOUXV.js.map