@sonicjs-cms/core 2.4.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/dist/{app-Db0AfT5F.d.cts → app-DV27cjPy.d.cts} +1 -1
  2. package/dist/{app-Db0AfT5F.d.ts → app-DV27cjPy.d.ts} +1 -1
  3. package/dist/{chunk-YIXSSJWD.cjs → chunk-63K7XXRX.cjs} +5 -5
  4. package/dist/{chunk-YIXSSJWD.cjs.map → chunk-63K7XXRX.cjs.map} +1 -1
  5. package/dist/{chunk-VNCYCH3H.js → chunk-7DL5SPPX.js} +59 -5
  6. package/dist/chunk-7DL5SPPX.js.map +1 -0
  7. package/dist/{chunk-AZLU3ROK.cjs → chunk-BZC4FYW7.cjs} +4 -4
  8. package/dist/chunk-BZC4FYW7.cjs.map +1 -0
  9. package/dist/chunk-CLIH2T74.js +403 -0
  10. package/dist/chunk-CLIH2T74.js.map +1 -0
  11. package/dist/{chunk-D2NLCPO2.js → chunk-EVZOVYLO.js} +53 -2
  12. package/dist/chunk-EVZOVYLO.js.map +1 -0
  13. package/dist/{chunk-DXM575E2.js → chunk-EYWR6UA2.js} +6 -6
  14. package/dist/chunk-EYWR6UA2.js.map +1 -0
  15. package/dist/{chunk-CPXAVWCU.js → chunk-F332TENF.js} +278 -3
  16. package/dist/chunk-F332TENF.js.map +1 -0
  17. package/dist/{chunk-FT6NBHNX.js → chunk-F6GZURXJ.js} +2536 -600
  18. package/dist/chunk-F6GZURXJ.js.map +1 -0
  19. package/dist/{chunk-2MI3LZFH.cjs → chunk-IIRVZSP2.cjs} +53 -2
  20. package/dist/chunk-IIRVZSP2.cjs.map +1 -0
  21. package/dist/{chunk-V5LBQN3I.js → chunk-KA2PDJNB.js} +4 -4
  22. package/dist/chunk-KA2PDJNB.js.map +1 -0
  23. package/dist/{chunk-AVPUX57O.js → chunk-KAOWRIFD.js} +3 -3
  24. package/dist/{chunk-AVPUX57O.js.map → chunk-KAOWRIFD.js.map} +1 -1
  25. package/dist/{chunk-ILZ3DP4I.cjs → chunk-MPT5PA6U.cjs} +24 -2
  26. package/dist/chunk-MPT5PA6U.cjs.map +1 -0
  27. package/dist/{chunk-A4SVOGG6.cjs → chunk-N7TDLOUE.cjs} +2696 -762
  28. package/dist/chunk-N7TDLOUE.cjs.map +1 -0
  29. package/dist/{chunk-7I5INVNR.cjs → chunk-T3YIKW2A.cjs} +9 -9
  30. package/dist/chunk-T3YIKW2A.cjs.map +1 -0
  31. package/dist/{chunk-DTLB6UIH.cjs → chunk-Y72M3MVX.cjs} +280 -2
  32. package/dist/chunk-Y72M3MVX.cjs.map +1 -0
  33. package/dist/{chunk-SGAG6FD3.js → chunk-YFJJU26H.js} +24 -2
  34. package/dist/chunk-YFJJU26H.js.map +1 -0
  35. package/dist/chunk-YHW27CBV.cjs +406 -0
  36. package/dist/chunk-YHW27CBV.cjs.map +1 -0
  37. package/dist/{chunk-FYEDK7K7.cjs → chunk-YMTTGHEK.cjs} +61 -4
  38. package/dist/chunk-YMTTGHEK.cjs.map +1 -0
  39. package/dist/{collection-config-FLlGtsh9.d.cts → collection-config-BF95LgQb.d.cts} +10 -2
  40. package/dist/{collection-config-FLlGtsh9.d.ts → collection-config-BF95LgQb.d.ts} +10 -2
  41. package/dist/index.cjs +2001 -142
  42. package/dist/index.cjs.map +1 -1
  43. package/dist/index.d.cts +504 -9
  44. package/dist/index.d.ts +504 -9
  45. package/dist/index.js +1893 -41
  46. package/dist/index.js.map +1 -1
  47. package/dist/middleware.cjs +24 -24
  48. package/dist/middleware.d.cts +1 -1
  49. package/dist/middleware.d.ts +1 -1
  50. package/dist/middleware.js +3 -3
  51. package/dist/migrations-QNYAWQLB.cjs +13 -0
  52. package/dist/{migrations-32QAYLWJ.cjs.map → migrations-QNYAWQLB.cjs.map} +1 -1
  53. package/dist/migrations-R6NQBKQV.js +4 -0
  54. package/dist/{migrations-57ZHBQ4X.js.map → migrations-R6NQBKQV.js.map} +1 -1
  55. package/dist/{plugin-bootstrap-CDh0JHtW.d.ts → plugin-bootstrap-CB-xaBfK.d.ts} +2 -2
  56. package/dist/{plugin-bootstrap-C0E3jdz-.d.cts → plugin-bootstrap-U-cw9jn3.d.cts} +2 -2
  57. package/dist/plugin-manager-Baa6xXqB.d.ts +328 -0
  58. package/dist/plugin-manager-vBal9Zip.d.cts +328 -0
  59. package/dist/plugins.cjs +20 -7
  60. package/dist/plugins.d.cts +53 -310
  61. package/dist/plugins.d.ts +53 -310
  62. package/dist/plugins.js +2 -1
  63. package/dist/routes.cjs +27 -26
  64. package/dist/routes.d.cts +1 -1
  65. package/dist/routes.d.ts +1 -1
  66. package/dist/routes.js +7 -6
  67. package/dist/services.cjs +16 -16
  68. package/dist/services.d.cts +2 -2
  69. package/dist/services.d.ts +2 -2
  70. package/dist/services.js +2 -2
  71. package/dist/templates.cjs +17 -17
  72. package/dist/templates.js +2 -2
  73. package/dist/types.d.cts +1 -1
  74. package/dist/types.d.ts +1 -1
  75. package/dist/utils.cjs +23 -11
  76. package/dist/utils.d.cts +38 -1
  77. package/dist/utils.d.ts +38 -1
  78. package/dist/utils.js +1 -1
  79. package/migrations/027_fix_slug_field_type.sql +18 -0
  80. package/migrations/028_fix_slug_field_type_in_schemas.sql +30 -0
  81. package/migrations/029_ai_search_plugin.sql +45 -0
  82. package/package.json +5 -2
  83. package/dist/chunk-2MI3LZFH.cjs.map +0 -1
  84. package/dist/chunk-7I5INVNR.cjs.map +0 -1
  85. package/dist/chunk-A4SVOGG6.cjs.map +0 -1
  86. package/dist/chunk-AZLU3ROK.cjs.map +0 -1
  87. package/dist/chunk-CPXAVWCU.js.map +0 -1
  88. package/dist/chunk-D2NLCPO2.js.map +0 -1
  89. package/dist/chunk-DTLB6UIH.cjs.map +0 -1
  90. package/dist/chunk-DXM575E2.js.map +0 -1
  91. package/dist/chunk-FT6NBHNX.js.map +0 -1
  92. package/dist/chunk-FYEDK7K7.cjs.map +0 -1
  93. package/dist/chunk-ILZ3DP4I.cjs.map +0 -1
  94. package/dist/chunk-SGAG6FD3.js.map +0 -1
  95. package/dist/chunk-V5LBQN3I.js.map +0 -1
  96. package/dist/chunk-VNCYCH3H.js.map +0 -1
  97. package/dist/migrations-32QAYLWJ.cjs +0 -13
  98. package/dist/migrations-57ZHBQ4X.js +0 -4
@@ -1,11 +1,12 @@
1
1
  'use strict';
2
2
 
3
3
  var chunk7FOAMNTI_cjs = require('./chunk-7FOAMNTI.cjs');
4
- var chunk7I5INVNR_cjs = require('./chunk-7I5INVNR.cjs');
5
- var chunkILZ3DP4I_cjs = require('./chunk-ILZ3DP4I.cjs');
6
- var chunk2MI3LZFH_cjs = require('./chunk-2MI3LZFH.cjs');
7
- var chunkAZLU3ROK_cjs = require('./chunk-AZLU3ROK.cjs');
8
- var chunkFYEDK7K7_cjs = require('./chunk-FYEDK7K7.cjs');
4
+ var chunkT3YIKW2A_cjs = require('./chunk-T3YIKW2A.cjs');
5
+ var chunkMPT5PA6U_cjs = require('./chunk-MPT5PA6U.cjs');
6
+ var chunkIIRVZSP2_cjs = require('./chunk-IIRVZSP2.cjs');
7
+ var chunkBZC4FYW7_cjs = require('./chunk-BZC4FYW7.cjs');
8
+ var chunkYHW27CBV_cjs = require('./chunk-YHW27CBV.cjs');
9
+ var chunkYMTTGHEK_cjs = require('./chunk-YMTTGHEK.cjs');
9
10
  var chunkRCQ2HIQD_cjs = require('./chunk-RCQ2HIQD.cjs');
10
11
  var hono = require('hono');
11
12
  var cors = require('hono/cors');
@@ -16,6 +17,37 @@ var html = require('hono/html');
16
17
  // src/schemas/index.ts
17
18
  var schemaDefinitions = [];
18
19
  var apiContentCrudRoutes = new hono.Hono();
20
+ apiContentCrudRoutes.get("/check-slug", async (c) => {
21
+ try {
22
+ const db = c.env.DB;
23
+ const collectionId = c.req.query("collectionId");
24
+ const slug = c.req.query("slug");
25
+ const excludeId = c.req.query("excludeId");
26
+ if (!collectionId || !slug) {
27
+ return c.json({ error: "collectionId and slug are required" }, 400);
28
+ }
29
+ let query = "SELECT id FROM content WHERE collection_id = ? AND slug = ?";
30
+ const params = [collectionId, slug];
31
+ if (excludeId) {
32
+ query += " AND id != ?";
33
+ params.push(excludeId);
34
+ }
35
+ const existing = await db.prepare(query).bind(...params).first();
36
+ if (existing) {
37
+ return c.json({
38
+ available: false,
39
+ message: "This URL slug is already in use in this collection"
40
+ });
41
+ }
42
+ return c.json({ available: true });
43
+ } catch (error) {
44
+ console.error("Error checking slug:", error);
45
+ return c.json({
46
+ error: "Failed to check slug availability",
47
+ details: error instanceof Error ? error.message : String(error)
48
+ }, 500);
49
+ }
50
+ });
19
51
  apiContentCrudRoutes.get("/:id", async (c) => {
20
52
  try {
21
53
  const id = c.req.param("id");
@@ -44,7 +76,7 @@ apiContentCrudRoutes.get("/:id", async (c) => {
44
76
  }, 500);
45
77
  }
46
78
  });
47
- apiContentCrudRoutes.post("/", chunk7I5INVNR_cjs.requireAuth(), async (c) => {
79
+ apiContentCrudRoutes.post("/", chunkT3YIKW2A_cjs.requireAuth(), async (c) => {
48
80
  try {
49
81
  const db = c.env.DB;
50
82
  const user = c.get("user");
@@ -110,7 +142,7 @@ apiContentCrudRoutes.post("/", chunk7I5INVNR_cjs.requireAuth(), async (c) => {
110
142
  }, 500);
111
143
  }
112
144
  });
113
- apiContentCrudRoutes.put("/:id", chunk7I5INVNR_cjs.requireAuth(), async (c) => {
145
+ apiContentCrudRoutes.put("/:id", chunkT3YIKW2A_cjs.requireAuth(), async (c) => {
114
146
  try {
115
147
  const id = c.req.param("id");
116
148
  const db = c.env.DB;
@@ -174,7 +206,7 @@ apiContentCrudRoutes.put("/:id", chunk7I5INVNR_cjs.requireAuth(), async (c) => {
174
206
  }, 500);
175
207
  }
176
208
  });
177
- apiContentCrudRoutes.delete("/:id", chunk7I5INVNR_cjs.requireAuth(), async (c) => {
209
+ apiContentCrudRoutes.delete("/:id", chunkT3YIKW2A_cjs.requireAuth(), async (c) => {
178
210
  try {
179
211
  const id = c.req.param("id");
180
212
  const db = c.env.DB;
@@ -210,7 +242,7 @@ apiRoutes.use("*", async (c, next) => {
210
242
  c.header("X-Response-Time", `${totalTime}ms`);
211
243
  });
212
244
  apiRoutes.use("*", async (c, next) => {
213
- const cacheEnabled = await chunk7I5INVNR_cjs.isPluginActive(c.env.DB, "core-cache");
245
+ const cacheEnabled = await chunkT3YIKW2A_cjs.isPluginActive(c.env.DB, "core-cache");
214
246
  c.set("cacheEnabled", cacheEnabled);
215
247
  await next();
216
248
  });
@@ -335,12 +367,12 @@ apiRoutes.get("/content", async (c) => {
335
367
  });
336
368
  }
337
369
  }
338
- const filter = chunkFYEDK7K7_cjs.QueryFilterBuilder.parseFromQuery(queryParams);
370
+ const filter = chunkYMTTGHEK_cjs.QueryFilterBuilder.parseFromQuery(queryParams);
339
371
  if (!filter.limit) {
340
372
  filter.limit = 50;
341
373
  }
342
374
  filter.limit = Math.min(filter.limit, 1e3);
343
- const builder3 = new chunkFYEDK7K7_cjs.QueryFilterBuilder();
375
+ const builder3 = new chunkYMTTGHEK_cjs.QueryFilterBuilder();
344
376
  const queryResult = builder3.build("content", filter);
345
377
  if (queryResult.errors.length > 0) {
346
378
  return c.json({
@@ -427,7 +459,7 @@ apiRoutes.get("/collections/:collection/content", async (c) => {
427
459
  if (!collectionResult) {
428
460
  return c.json({ error: "Collection not found" }, 404);
429
461
  }
430
- const filter = chunkFYEDK7K7_cjs.QueryFilterBuilder.parseFromQuery(queryParams);
462
+ const filter = chunkYMTTGHEK_cjs.QueryFilterBuilder.parseFromQuery(queryParams);
431
463
  if (!filter.where) {
432
464
  filter.where = { and: [] };
433
465
  }
@@ -443,7 +475,7 @@ apiRoutes.get("/collections/:collection/content", async (c) => {
443
475
  filter.limit = 50;
444
476
  }
445
477
  filter.limit = Math.min(filter.limit, 1e3);
446
- const builder3 = new chunkFYEDK7K7_cjs.QueryFilterBuilder();
478
+ const builder3 = new chunkYMTTGHEK_cjs.QueryFilterBuilder();
447
479
  const queryResult = builder3.build("content", filter);
448
480
  if (queryResult.errors.length > 0) {
449
481
  return c.json({
@@ -568,7 +600,7 @@ var fileValidationSchema = zod.z.object({
568
600
  // 50MB max
569
601
  });
570
602
  var apiMediaRoutes = new hono.Hono();
571
- apiMediaRoutes.use("*", chunk7I5INVNR_cjs.requireAuth());
603
+ apiMediaRoutes.use("*", chunkT3YIKW2A_cjs.requireAuth());
572
604
  apiMediaRoutes.post("/upload", async (c) => {
573
605
  try {
574
606
  const user = c.get("user");
@@ -1312,8 +1344,8 @@ apiSystemRoutes.get("/env", (c) => {
1312
1344
  });
1313
1345
  var api_system_default = apiSystemRoutes;
1314
1346
  var adminApiRoutes = new hono.Hono();
1315
- adminApiRoutes.use("*", chunk7I5INVNR_cjs.requireAuth());
1316
- adminApiRoutes.use("*", chunk7I5INVNR_cjs.requireRole(["admin", "editor"]));
1347
+ adminApiRoutes.use("*", chunkT3YIKW2A_cjs.requireAuth());
1348
+ adminApiRoutes.use("*", chunkT3YIKW2A_cjs.requireRole(["admin", "editor"]));
1317
1349
  adminApiRoutes.get("/stats", async (c) => {
1318
1350
  try {
1319
1351
  const db = c.env.DB;
@@ -1553,6 +1585,107 @@ adminApiRoutes.get("/collections/:id", async (c) => {
1553
1585
  return c.json({ error: "Failed to fetch collection" }, 500);
1554
1586
  }
1555
1587
  });
1588
+ adminApiRoutes.get("/references", async (c) => {
1589
+ try {
1590
+ const db = c.env.DB;
1591
+ const url = new URL(c.req.url);
1592
+ const collectionParams = url.searchParams.getAll("collection").flatMap((value) => value.split(",")).map((value) => value.trim()).filter(Boolean);
1593
+ const search = c.req.query("search") || "";
1594
+ const id = c.req.query("id") || "";
1595
+ const limit = Math.min(Number.parseInt(c.req.query("limit") || "20", 10) || 20, 100);
1596
+ if (collectionParams.length === 0) {
1597
+ return c.json({ error: "Collection is required" }, 400);
1598
+ }
1599
+ const placeholders = collectionParams.map(() => "?").join(", ");
1600
+ const collectionStmt = db.prepare(`
1601
+ SELECT id, name, display_name
1602
+ FROM collections
1603
+ WHERE id IN (${placeholders}) OR name IN (${placeholders})
1604
+ `);
1605
+ const collectionResults = await collectionStmt.bind(...collectionParams, ...collectionParams).all();
1606
+ const collections = collectionResults.results || [];
1607
+ if (collections.length === 0) {
1608
+ return c.json({ error: "Collection not found" }, 404);
1609
+ }
1610
+ const collectionById = Object.fromEntries(
1611
+ collections.map((entry) => [
1612
+ entry.id,
1613
+ {
1614
+ id: entry.id,
1615
+ name: entry.name,
1616
+ display_name: entry.display_name
1617
+ }
1618
+ ])
1619
+ );
1620
+ const collectionIds = collections.map((entry) => entry.id);
1621
+ if (id) {
1622
+ const idPlaceholders = collectionIds.map(() => "?").join(", ");
1623
+ const itemStmt = db.prepare(`
1624
+ SELECT id, title, slug, collection_id
1625
+ FROM content
1626
+ WHERE id = ? AND collection_id IN (${idPlaceholders})
1627
+ LIMIT 1
1628
+ `);
1629
+ const item = await itemStmt.bind(id, ...collectionIds).first();
1630
+ if (!item) {
1631
+ return c.json({ error: "Reference not found" }, 404);
1632
+ }
1633
+ return c.json({
1634
+ data: {
1635
+ id: item.id,
1636
+ title: item.title,
1637
+ slug: item.slug,
1638
+ collection: collectionById[item.collection_id]
1639
+ }
1640
+ });
1641
+ }
1642
+ let stmt;
1643
+ let results;
1644
+ const listPlaceholders = collectionIds.map(() => "?").join(", ");
1645
+ const statusFilterValues = ["published"];
1646
+ const statusClause = ` AND status IN (${statusFilterValues.map(() => "?").join(", ")})`;
1647
+ if (search) {
1648
+ const searchParam = `%${search}%`;
1649
+ stmt = db.prepare(`
1650
+ SELECT id, title, slug, status, updated_at, collection_id
1651
+ FROM content
1652
+ WHERE collection_id IN (${listPlaceholders})
1653
+ AND (title LIKE ? OR slug LIKE ?)
1654
+ ${statusClause}
1655
+ ORDER BY updated_at DESC
1656
+ LIMIT ?
1657
+ `);
1658
+ const queryResults = await stmt.bind(...collectionIds, searchParam, searchParam, ...statusFilterValues, limit).all();
1659
+ results = queryResults.results;
1660
+ } else {
1661
+ stmt = db.prepare(`
1662
+ SELECT id, title, slug, status, updated_at, collection_id
1663
+ FROM content
1664
+ WHERE collection_id IN (${listPlaceholders})
1665
+ ${statusClause}
1666
+ ORDER BY updated_at DESC
1667
+ LIMIT ?
1668
+ `);
1669
+ const queryResults = await stmt.bind(...collectionIds, ...statusFilterValues, limit).all();
1670
+ results = queryResults.results;
1671
+ }
1672
+ const items = (results || []).map((row) => ({
1673
+ id: row.id,
1674
+ title: row.title,
1675
+ slug: row.slug,
1676
+ status: row.status,
1677
+ updated_at: row.updated_at ? Number(row.updated_at) : null,
1678
+ collection: collectionById[row.collection_id]
1679
+ }));
1680
+ return c.json({
1681
+ data: items,
1682
+ count: items.length
1683
+ });
1684
+ } catch (error) {
1685
+ console.error("Error fetching reference options:", error);
1686
+ return c.json({ error: "Failed to fetch references" }, 500);
1687
+ }
1688
+ });
1556
1689
  adminApiRoutes.post("/collections", async (c) => {
1557
1690
  try {
1558
1691
  const contentType = c.req.header("Content-Type");
@@ -1722,7 +1855,7 @@ adminApiRoutes.delete("/collections/:id", async (c) => {
1722
1855
  });
1723
1856
  adminApiRoutes.get("/migrations/status", async (c) => {
1724
1857
  try {
1725
- const { MigrationService: MigrationService2 } = await import('./migrations-32QAYLWJ.cjs');
1858
+ const { MigrationService: MigrationService2 } = await import('./migrations-QNYAWQLB.cjs');
1726
1859
  const db = c.env.DB;
1727
1860
  const migrationService = new MigrationService2(db);
1728
1861
  const status = await migrationService.getMigrationStatus();
@@ -1747,7 +1880,7 @@ adminApiRoutes.post("/migrations/run", async (c) => {
1747
1880
  error: "Unauthorized. Admin access required."
1748
1881
  }, 403);
1749
1882
  }
1750
- const { MigrationService: MigrationService2 } = await import('./migrations-32QAYLWJ.cjs');
1883
+ const { MigrationService: MigrationService2 } = await import('./migrations-QNYAWQLB.cjs');
1751
1884
  const db = c.env.DB;
1752
1885
  const migrationService = new MigrationService2(db);
1753
1886
  const result = await migrationService.runPendingMigrations();
@@ -1766,7 +1899,7 @@ adminApiRoutes.post("/migrations/run", async (c) => {
1766
1899
  });
1767
1900
  adminApiRoutes.get("/migrations/validate", async (c) => {
1768
1901
  try {
1769
- const { MigrationService: MigrationService2 } = await import('./migrations-32QAYLWJ.cjs');
1902
+ const { MigrationService: MigrationService2 } = await import('./migrations-QNYAWQLB.cjs');
1770
1903
  const db = c.env.DB;
1771
1904
  const migrationService = new MigrationService2(db);
1772
1905
  const validation = await migrationService.validateSchema();
@@ -1793,7 +1926,7 @@ function renderLoginPage(data, demoLoginActive = false) {
1793
1926
  <meta charset="UTF-8">
1794
1927
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
1795
1928
  <title>Login - SonicJS AI</title>
1796
- <link rel="icon" type="image/x-icon" href="https://demo.sonicjs.com/images/favicon.ico">
1929
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg">
1797
1930
  <script src="https://unpkg.com/htmx.org@2.0.3"></script>
1798
1931
  <script src="https://cdn.tailwindcss.com"></script>
1799
1932
  <script>
@@ -1841,8 +1974,8 @@ function renderLoginPage(data, demoLoginActive = false) {
1841
1974
  <div class="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
1842
1975
  <div class="bg-zinc-900 shadow-sm ring-1 ring-white/10 rounded-xl px-6 py-8 sm:px-10">
1843
1976
  <!-- Alerts -->
1844
- ${data.error ? `<div class="mb-6">${chunkAZLU3ROK_cjs.renderAlert({ type: "error", message: data.error })}</div>` : ""}
1845
- ${data.message ? `<div class="mb-6">${chunkAZLU3ROK_cjs.renderAlert({ type: "success", message: data.message })}</div>` : ""}
1977
+ ${data.error ? `<div class="mb-6">${chunkBZC4FYW7_cjs.renderAlert({ type: "error", message: data.error })}</div>` : ""}
1978
+ ${data.message ? `<div class="mb-6">${chunkBZC4FYW7_cjs.renderAlert({ type: "success", message: data.message })}</div>` : ""}
1846
1979
 
1847
1980
  <!-- Form Response (HTMX target) -->
1848
1981
  <div id="form-response" class="mb-6"></div>
@@ -1970,7 +2103,7 @@ function renderRegisterPage(data) {
1970
2103
  <meta charset="UTF-8">
1971
2104
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
1972
2105
  <title>Register - SonicJS AI</title>
1973
- <link rel="icon" type="image/x-icon" href="https://demo.sonicjs.com/images/favicon.ico">
2106
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg">
1974
2107
  <script src="https://unpkg.com/htmx.org@2.0.3"></script>
1975
2108
  <script src="https://cdn.tailwindcss.com"></script>
1976
2109
  <script>
@@ -1993,42 +2126,20 @@ function renderRegisterPage(data) {
1993
2126
  <div class="flex min-h-full flex-col justify-center py-12 sm:px-6 lg:px-8">
1994
2127
  <!-- Logo Section -->
1995
2128
  <div class="sm:mx-auto sm:w-full sm:max-w-md text-center">
1996
- <div class="mx-auto w-64 mb-8">
1997
- <svg class="w-full h-auto" viewBox="380 1300 2250 400" aria-hidden="true">
1998
- <path fill="#F1F2F2" d="M476.851,1404.673h168.536c4.714,0,8.695-1.618,11.944-4.866c3.241-3.241,4.866-7.222,4.866-11.943 c0-2.357-0.443-4.569-1.327-6.636c-0.885-2.06-2.067-3.829-3.539-5.308c-1.479-1.472-3.249-2.654-5.308-3.538 c-2.067-0.885-4.279-1.327-6.635-1.327H476.851c-20.057,0-37.158,7.154-51.313,21.454c-14.155,14.308-21.233,31.483-21.233,51.534 c0,20.058,7.078,37.234,21.233,51.534c14.155,14.308,31.255,21.454,51.313,21.454h112.357c10.907,0,20.196,3.837,27.868,11.502 c7.666,7.672,11.502,16.885,11.502,27.646c0,10.769-3.836,19.982-11.502,27.647c-7.672,7.673-16.961,11.502-27.868,11.502H421.115 c-4.721,0-8.702,1.624-11.944,4.865c-3.248,3.249-4.866,7.23-4.866,11.944c0,3.248,0.733,6.123,2.212,8.626 c1.472,2.509,3.462,4.499,5.971,5.972c2.502,1.472,5.378,2.212,8.626,2.212h168.094c20.052,0,37.227-7.078,51.534-21.234 c14.3-14.155,21.454-31.331,21.454-51.534c0-20.196-7.154-37.379-21.454-51.534c-14.308-14.156-31.483-21.234-51.534-21.234 H476.851c-10.616,0-19.76-3.905-27.426-11.721c-7.672-7.811-11.501-17.101-11.501-27.87c0-10.761,3.829-19.975,11.501-27.647 C457.091,1408.508,466.235,1404.673,476.851,1404.673z"></path>
1999
- <path fill="#F1F2F2" d="M974.78,1398.211c-5.016,6.574-10.034,13.146-15.048,19.721c-1.828,2.398-3.657,4.796-5.487,7.194 c1.994,1.719,3.958,3.51,5.873,5.424c18.724,18.731,28.089,41.216,28.089,67.459c0,26.251-9.366,48.658-28.089,67.237 c-18.731,18.579-41.215,27.868-67.459,27.868c-9.848,0-19.156-1.308-27.923-3.923l-4.185,3.354 c-8.587,6.885-17.154,13.796-25.725,20.702c17.52,8.967,36.86,13.487,58.054,13.487c35.533,0,65.91-12.608,91.124-37.821 c25.214-25.215,37.821-55.584,37.821-91.125c0-35.534-12.607-65.911-37.821-91.126 C981.004,1403.663,977.926,1400.854,974.78,1398.211z"></path>
2000
- <path fill="#F1F2F2" d="M1364.644,1439.619c-4.72,0-8.702,1.624-11.943,4.865c-3.249,3.249-4.866,7.23-4.866,11.944v138.014 l-167.651-211.003c-0.297-0.586-0.74-1.03-1.327-1.326c-4.721-4.714-10.249-7.742-16.588-9.069 c-6.346-1.326-12.608-0.732-18.801,1.77c-6.192,2.509-11.059,6.49-14.598,11.944c-3.539,5.46-5.308,11.577-5.308,18.357v208.348 c0,4.721,1.618,8.703,4.866,11.944c3.241,3.241,7.222,4.865,11.943,4.865c2.945,0,5.751-0.738,8.405-2.211 c2.654-1.472,4.713-3.463,6.193-5.971c1.473-2.503,2.212-5.378,2.212-8.627v-205.251l166.325,209.675 c2.06,2.654,4.423,4.865,7.078,6.635c5.308,3.829,11.349,5.75,18.137,5.75c5.308,0,10.464-1.182,15.482-3.538 c3.539-1.769,6.56-4.127,9.069-7.078c2.502-2.945,4.491-6.338,5.971-10.175c1.473-3.829,2.212-7.664,2.212-11.501v-141.552 c0-4.714-1.624-8.695-4.865-11.944C1373.339,1441.243,1369.359,1439.619,1364.644,1439.619z"></path>
2001
- <path fill="#F1F2F2" d="M1508.406,1432.983c-2.654-1.472-5.46-2.212-8.404-2.212c-4.721,0-8.703,1.7-11.944,5.087 c-3.249,3.395-4.865,7.3-4.865,11.723v163.228c0,4.721,1.616,8.702,4.865,11.943c3.241,3.249,7.223,4.866,11.944,4.866 c2.944,0,5.751-0.732,8.404-2.212c2.655-1.472,4.714-3.539,6.193-6.194c1.473-2.654,2.213-5.453,2.213-8.404V1447.58 c0-2.945-0.74-5.75-2.213-8.405C1513.12,1436.522,1511.06,1434.462,1508.406,1432.983z"></path>
2002
- <path fill="#F1F2F2" d="M1499.78,1367.957c-4.575,0-8.481,1.625-11.722,4.866c-3.249,3.249-4.865,7.23-4.865,11.943 c0,2.951,0.732,5.75,2.212,8.405c1.472,2.654,3.463,4.721,5.971,6.193c2.503,1.479,5.378,2.212,8.627,2.212 c4.423,0,8.328-1.618,11.721-4.865c3.387-3.243,5.088-7.224,5.088-11.944c0-4.713-1.701-8.694-5.088-11.943 C1508.33,1369.582,1504.349,1367.957,1499.78,1367.957z"></path>
2003
- <path fill="#F1F2F2" d="M1859.627,1369.727H1747.27c-35.388,0-65.69,12.607-90.904,37.821 c-25.213,25.215-37.82,55.591-37.82,91.125c0,35.54,12.607,65.911,37.82,91.125c25.215,25.215,55.516,37.821,90.904,37.821h56.178 c4.714,0,8.695-1.618,11.944-4.866c3.241-3.241,4.865-7.222,4.865-11.943c0-4.714-1.624-8.695-4.865-11.943 c-3.249-3.243-7.23-4.866-11.944-4.866h-56.178c-26.251,0-48.659-9.359-67.237-28.09c-18.579-18.723-27.868-41.207-27.868-67.459 c0-26.243,9.29-48.659,27.868-67.237c18.579-18.579,40.987-27.868,67.237-27.868h112.357c4.714,0,8.696-1.693,11.944-5.087 c3.241-3.387,4.865-7.368,4.865-11.943c0-4.569-1.624-8.475-4.865-11.723C1868.322,1371.351,1864.341,1369.727,1859.627,1369.727z "></path>
2004
- <path fill="#06b6d4" d="M2219.256,1371.054h-112.357c-4.423,0-8.336,1.624-11.723,4.865c-3.393,3.249-5.087,7.23-5.087,11.944 c0,4.721,1.694,8.702,5.087,11.943c3.387,3.249,7.3,4.866,11.723,4.866h95.547v95.105c0,26.251-9.365,48.659-28.088,67.237 c-18.731,18.579-41.215,27.868-67.459,27.868c-26.251,0-48.659-9.289-67.237-27.868c-18.579-18.579-27.868-40.987-27.868-67.237 c0-4.713-1.701-8.771-5.088-12.165c-3.393-3.387-7.374-5.087-11.943-5.087c-4.575,0-8.481,1.7-11.722,5.087 c-3.249,3.393-4.865,7.451-4.865,12.165c0,35.388,12.607,65.69,37.82,90.904c25.215,25.213,55.584,37.82,91.126,37.82 c35.532,0,65.91-12.607,91.125-37.82c25.214-25.215,37.82-55.516,37.82-90.904v-111.915c0-4.714-1.624-8.695-4.865-11.944 C2227.951,1372.678,2223.971,1371.054,2219.256,1371.054z"></path>
2005
- <path fill="#06b6d4" d="M2574.24,1502.875c-14.306-14.156-31.483-21.234-51.533-21.234H2410.35 c-10.617,0-19.762-3.829-27.426-11.501c-7.672-7.664-11.501-16.954-11.501-27.868c0-10.907,3.829-20.196,11.501-27.868 c7.664-7.664,16.809-11.501,27.426-11.501h112.357c4.714,0,8.695-1.617,11.944-4.866c3.241-3.241,4.865-7.222,4.865-11.943 c0-4.714-1.624-8.695-4.865-11.944c-3.249-3.241-7.23-4.865-11.944-4.865H2410.35c-20.058,0-37.158,7.154-51.313,21.454 c-14.156,14.308-21.232,31.483-21.232,51.534c0,20.058,7.077,37.234,21.232,51.534c14.156,14.308,31.255,21.454,51.313,21.454 h112.357c7.078,0,13.637,1.77,19.684,5.308c6.042,3.539,10.838,8.336,14.377,14.377c3.538,6.047,5.307,12.607,5.307,19.685 c0,10.616-3.835,19.76-11.501,27.425c-7.672,7.673-16.961,11.502-27.868,11.502h-168.094c-4.721,0-8.703,1.7-11.944,5.087 c-3.249,3.393-4.865,7.374-4.865,11.943c0,4.576,1.616,8.481,4.865,11.723c3.241,3.249,7.223,4.866,11.944,4.866h168.094 c20.051,0,37.227-7.078,51.533-21.234c14.302-14.155,21.454-31.331,21.454-51.534 C2595.695,1534.213,2588.542,1517.03,2574.24,1502.875z"></path>
2006
- <path fill="#06b6d4" d="M854.024,1585.195l20.001-16.028c16.616-13.507,33.04-27.265,50.086-40.251 c1.13-0.861,2.9-1.686,2.003-3.516c-0.843-1.716-2.481-2.302-4.484-2.123c-8.514,0.765-17.016-0.538-25.537-0.353 c-1.124,0.024-2.768,0.221-3.163-1.25c-0.371-1.369,1.088-2.063,1.919-2.894c6.26-6.242,12.574-12.43,18.816-18.691 c9.303-9.327,18.565-18.714,27.851-28.066c1.848-1.859,3.701-3.713,5.549-5.572c2.655-2.661,5.309-5.315,7.958-7.982 c0.574-0.579,1.259-1.141,1.246-1.94c-0.004-0.257-0.078-0.538-0.254-0.853c-0.556-0.981-1.441-1.1-2.469-0.957 c-0.658,0.096-1.315,0.185-1.973,0.275c-3.844,0.538-7.689,1.076-11.533,1.608c-3.641,0.505-7.281,1.02-10.922,1.529 c-4.162,0.582-8.324,1.158-12.486,1.748c-1.142,0.161-2.409,1.662-3.354,0.508c-0.419-0.508-0.431-1.028-0.251-1.531 c0.269-0.741,0.957-1.441,1.387-2.021c3.414-4.58,6.882-9.124,10.356-13.662c1.74-2.272,3.48-4.544,5.214-6.822 c4.682-6.141,9.369-12.281,14.051-18.422c0.09-0.119,0.181-0.237,0.271-0.355c6.848-8.98,13.7-17.958,20.553-26.936 c0.488-0.64,0.977-1.28,1.465-1.92c2.159-2.828,4.315-5.658,6.476-8.486c4.197-5.501,8.454-10.954,12.67-16.442 c0.263-0.347,0.538-0.718,0.717-1.106c0.269-0.586,0.299-1.196-0.335-1.776c-0.825-0.753-1.8-0.15-2.595,0.419 c-0.67,0.472-1.333,0.957-1.955,1.489c-2.206,1.889-4.401,3.797-6.595,5.698c-3.958,3.438-7.922,6.876-11.976,10.194 c-2.443,2.003-4.865,4.028-7.301,6.038c-18.689-10.581-39.53-15.906-62.549-15.906c-35.54,0-65.911,12.607-91.125,37.82 c-25.214,25.215-37.821,55.592-37.821,91.126c0,35.54,12.607,65.91,37.821,91.125c4.146,4.146,8.445,7.916,12.87,11.381 c-9.015,11.14-18.036,22.277-27.034,33.429c-1.208,1.489-3.755,3.151-2.745,4.891c0.078,0.144,0.173,0.281,0.305,0.425 c1.321,1.429,3.492-1.303,4.933-2.457c6.673-5.333,13.333-10.685,19.982-16.042c3.707-2.984,7.417-5.965,11.124-8.952 c1.474-1.188,2.951-2.373,4.425-3.561c6.41-5.164,12.816-10.333,19.238-15.481L854.024,1585.195z M797.552,1498.009 c0-26.243,9.29-48.728,27.868-67.459c18.579-18.723,40.987-28.089,67.238-28.089c12.273,0,23.712,2.075,34.34,6.171 c-3.37,2.905-6.734,5.816-10.069,8.762c-6.075,5.351-12.365,10.469-18.667,15.564c-4.179,3.378-8.371,6.744-12.514,10.164 c-7.54,6.23-15.037,12.52-22.529,18.804c-7.091,5.955-14.182,11.904-21.19,17.949c-1.136,0.974-3.055,1.907-2.135,3.94 c0.831,1.836,2.774,1.417,4.341,1.578l12.145-0.599l14.151-0.698c1.031-0.102,2.192-0.257,2.89,0.632 c0.034,0.044,0.073,0.078,0.106,0.127c1.017,1.561-0.67,2.105-1.387,2.942c-6.308,7.318-12.616,14.637-18.978,21.907 c-8.161,9.339-16.353,18.649-24.544,27.958c-2.146,2.433-4.275,4.879-6.422,7.312c-1.034,1.172-2.129,2.272-1.238,3.922 c0.933,1.728,2.685,1.752,4.323,1.602c4.134-0.367,8.263-0.489,12.396-0.492c0.242,0,0.485-0.005,0.728-0.004 c2.711,0.009,5.422,0.068,8.134,0.145c2.582,0.074,5.166,0.165,7.752,0.249c0.275,1.62-0.879,2.356-1.62,3.259 c-1.333,1.626-2.667,3.247-4,4.867c-4.315,5.252-8.62,10.514-12.928,15.772c-3.562-2.725-7.007-5.733-10.324-9.051 C806.842,1546.667,797.552,1524.26,797.552,1498.009z"></path>
2129
+ <div class="mx-auto flex h-12 w-12 items-center justify-center rounded-lg bg-white">
2130
+ <svg class="h-7 w-7 text-zinc-950" fill="none" stroke="currentColor" viewBox="0 0 24 24">
2131
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
2007
2132
  </svg>
2008
2133
  </div>
2009
- <h2 class="mt-6 text-xl font-medium text-white">${data.isSetup ? "Welcome to SonicJS" : "Create Account"}</h2>
2010
- ${data.isSetup ? `<p class="mt-2 text-sm text-zinc-400">Create your admin account to get started.</p>` : `<p class="mt-2 text-sm text-zinc-400">Create your account and get started</p>`}
2134
+ <h1 class="mt-6 text-3xl font-semibold tracking-tight text-white">SonicJS AI</h1>
2135
+ <p class="mt-2 text-sm text-zinc-400">Create your account and get started</p>
2011
2136
  </div>
2012
2137
 
2013
2138
  <!-- Form Container -->
2014
2139
  <div class="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
2015
2140
  <div class="bg-zinc-900 shadow-sm ring-1 ring-white/10 rounded-xl px-6 py-8 sm:px-10">
2016
- <!-- Setup Banner -->
2017
- ${data.isSetup ? `
2018
- <div class="mb-6 rounded-lg bg-blue-500/10 p-4 ring-1 ring-blue-500/20">
2019
- <div class="flex items-start gap-x-3">
2020
- <svg class="h-5 w-5 text-blue-400 shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
2021
- <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"/>
2022
- </svg>
2023
- <div class="flex-1">
2024
- <p class="text-sm font-medium text-blue-300">First-time Setup</p>
2025
- <p class="text-sm text-blue-400/80 mt-1">This account will be the administrator with full access to manage your SonicJS installation.</p>
2026
- </div>
2027
- </div>
2028
- </div>
2029
- ` : ""}
2030
2141
  <!-- Alerts -->
2031
- ${data.error ? `<div class="mb-6">${chunkAZLU3ROK_cjs.renderAlert({ type: "error", message: data.error })}</div>` : ""}
2142
+ ${data.error ? `<div class="mb-6">${chunkBZC4FYW7_cjs.renderAlert({ type: "error", message: data.error })}</div>` : ""}
2032
2143
 
2033
2144
  <!-- Form -->
2034
2145
  <form
@@ -2141,7 +2252,6 @@ function renderRegisterPage(data) {
2141
2252
  </html>
2142
2253
  `;
2143
2254
  }
2144
- var adminExistsCache = null;
2145
2255
  async function isRegistrationEnabled(db) {
2146
2256
  try {
2147
2257
  const plugin = await db.prepare("SELECT settings FROM plugins WHERE id = ?").bind("core-auth").first();
@@ -2163,21 +2273,6 @@ async function isFirstUserRegistration(db) {
2163
2273
  return false;
2164
2274
  }
2165
2275
  }
2166
- async function checkAdminUserExists(db) {
2167
- if (adminExistsCache !== null) {
2168
- return adminExistsCache;
2169
- }
2170
- try {
2171
- const result = await db.prepare("SELECT id FROM users WHERE role = ?").bind("admin").first();
2172
- adminExistsCache = !!result;
2173
- return adminExistsCache;
2174
- } catch {
2175
- return false;
2176
- }
2177
- }
2178
- function setAdminExists() {
2179
- adminExistsCache = true;
2180
- }
2181
2276
  var baseRegistrationSchema = zod.z.object({
2182
2277
  email: zod.z.string().email("Valid email is required"),
2183
2278
  password: zod.z.string().min(8, "Password must be at least 8 characters"),
@@ -2239,11 +2334,8 @@ authRoutes.get("/register", async (c) => {
2239
2334
  }
2240
2335
  }
2241
2336
  const error = c.req.query("error");
2242
- const isSetup = c.req.query("setup") === "true";
2243
2337
  const pageData = {
2244
- error: error || void 0,
2245
- isSetup: isSetup && isFirstUser
2246
- // Only show setup message if truly first user
2338
+ error: error || void 0
2247
2339
  };
2248
2340
  return c.html(renderRegisterPage(pageData));
2249
2341
  });
@@ -2289,7 +2381,7 @@ authRoutes.post(
2289
2381
  if (existingUser) {
2290
2382
  return c.json({ error: "User with this email or username already exists" }, 400);
2291
2383
  }
2292
- const passwordHash = await chunk7I5INVNR_cjs.AuthManager.hashPassword(password);
2384
+ const passwordHash = await chunkT3YIKW2A_cjs.AuthManager.hashPassword(password);
2293
2385
  const userId = crypto.randomUUID();
2294
2386
  const now = /* @__PURE__ */ new Date();
2295
2387
  await db.prepare(`
@@ -2309,7 +2401,7 @@ authRoutes.post(
2309
2401
  now.getTime(),
2310
2402
  now.getTime()
2311
2403
  ).run();
2312
- const token = await chunk7I5INVNR_cjs.AuthManager.generateToken(userId, normalizedEmail, "viewer");
2404
+ const token = await chunkT3YIKW2A_cjs.AuthManager.generateToken(userId, normalizedEmail, "viewer");
2313
2405
  cookie.setCookie(c, "auth_token", token, {
2314
2406
  httpOnly: true,
2315
2407
  secure: true,
@@ -2362,11 +2454,11 @@ authRoutes.post("/login", async (c) => {
2362
2454
  if (!user) {
2363
2455
  return c.json({ error: "Invalid email or password" }, 401);
2364
2456
  }
2365
- const isValidPassword = await chunk7I5INVNR_cjs.AuthManager.verifyPassword(password, user.password_hash);
2457
+ const isValidPassword = await chunkT3YIKW2A_cjs.AuthManager.verifyPassword(password, user.password_hash);
2366
2458
  if (!isValidPassword) {
2367
2459
  return c.json({ error: "Invalid email or password" }, 401);
2368
2460
  }
2369
- const token = await chunk7I5INVNR_cjs.AuthManager.generateToken(user.id, user.email, user.role);
2461
+ const token = await chunkT3YIKW2A_cjs.AuthManager.generateToken(user.id, user.email, user.role);
2370
2462
  cookie.setCookie(c, "auth_token", token, {
2371
2463
  httpOnly: true,
2372
2464
  secure: true,
@@ -2415,7 +2507,7 @@ authRoutes.get("/logout", (c) => {
2415
2507
  });
2416
2508
  return c.redirect("/auth/login?message=You have been logged out successfully");
2417
2509
  });
2418
- authRoutes.get("/me", chunk7I5INVNR_cjs.requireAuth(), async (c) => {
2510
+ authRoutes.get("/me", chunkT3YIKW2A_cjs.requireAuth(), async (c) => {
2419
2511
  try {
2420
2512
  const user = c.get("user");
2421
2513
  if (!user) {
@@ -2432,13 +2524,13 @@ authRoutes.get("/me", chunk7I5INVNR_cjs.requireAuth(), async (c) => {
2432
2524
  return c.json({ error: "Failed to get user" }, 500);
2433
2525
  }
2434
2526
  });
2435
- authRoutes.post("/refresh", chunk7I5INVNR_cjs.requireAuth(), async (c) => {
2527
+ authRoutes.post("/refresh", chunkT3YIKW2A_cjs.requireAuth(), async (c) => {
2436
2528
  try {
2437
2529
  const user = c.get("user");
2438
2530
  if (!user) {
2439
2531
  return c.json({ error: "Not authenticated" }, 401);
2440
2532
  }
2441
- const token = await chunk7I5INVNR_cjs.AuthManager.generateToken(user.userId, user.email, user.role);
2533
+ const token = await chunkT3YIKW2A_cjs.AuthManager.generateToken(user.userId, user.email, user.role);
2442
2534
  cookie.setCookie(c, "auth_token", token, {
2443
2535
  httpOnly: true,
2444
2536
  secure: true,
@@ -2498,7 +2590,7 @@ authRoutes.post("/register/form", async (c) => {
2498
2590
  </div>
2499
2591
  `);
2500
2592
  }
2501
- const passwordHash = await chunk7I5INVNR_cjs.AuthManager.hashPassword(password);
2593
+ const passwordHash = await chunkT3YIKW2A_cjs.AuthManager.hashPassword(password);
2502
2594
  const role = isFirstUser ? "admin" : "viewer";
2503
2595
  const userId = crypto.randomUUID();
2504
2596
  const now = /* @__PURE__ */ new Date();
@@ -2518,10 +2610,7 @@ authRoutes.post("/register/form", async (c) => {
2518
2610
  now.getTime(),
2519
2611
  now.getTime()
2520
2612
  ).run();
2521
- if (isFirstUser) {
2522
- setAdminExists();
2523
- }
2524
- const token = await chunk7I5INVNR_cjs.AuthManager.generateToken(userId, normalizedEmail, role);
2613
+ const token = await chunkT3YIKW2A_cjs.AuthManager.generateToken(userId, normalizedEmail, role);
2525
2614
  cookie.setCookie(c, "auth_token", token, {
2526
2615
  httpOnly: true,
2527
2616
  secure: false,
@@ -2573,7 +2662,7 @@ authRoutes.post("/login/form", async (c) => {
2573
2662
  </div>
2574
2663
  `);
2575
2664
  }
2576
- const isValidPassword = await chunk7I5INVNR_cjs.AuthManager.verifyPassword(password, user.password_hash);
2665
+ const isValidPassword = await chunkT3YIKW2A_cjs.AuthManager.verifyPassword(password, user.password_hash);
2577
2666
  if (!isValidPassword) {
2578
2667
  return c.html(html.html`
2579
2668
  <div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
@@ -2581,7 +2670,7 @@ authRoutes.post("/login/form", async (c) => {
2581
2670
  </div>
2582
2671
  `);
2583
2672
  }
2584
- const token = await chunk7I5INVNR_cjs.AuthManager.generateToken(user.id, user.email, user.role);
2673
+ const token = await chunkT3YIKW2A_cjs.AuthManager.generateToken(user.id, user.email, user.role);
2585
2674
  cookie.setCookie(c, "auth_token", token, {
2586
2675
  httpOnly: true,
2587
2676
  secure: false,
@@ -2640,9 +2729,8 @@ authRoutes.post("/seed-admin", async (c) => {
2640
2729
  `).run();
2641
2730
  const existingAdmin = await db.prepare("SELECT id FROM users WHERE email = ? OR username = ?").bind("admin@sonicjs.com", "admin").first();
2642
2731
  if (existingAdmin) {
2643
- const passwordHash2 = await chunk7I5INVNR_cjs.AuthManager.hashPassword("sonicjs!");
2732
+ const passwordHash2 = await chunkT3YIKW2A_cjs.AuthManager.hashPassword("sonicjs!");
2644
2733
  await db.prepare("UPDATE users SET password_hash = ?, updated_at = ? WHERE id = ?").bind(passwordHash2, Date.now(), existingAdmin.id).run();
2645
- setAdminExists();
2646
2734
  return c.json({
2647
2735
  message: "Admin user already exists (password updated)",
2648
2736
  user: {
@@ -2653,7 +2741,7 @@ authRoutes.post("/seed-admin", async (c) => {
2653
2741
  }
2654
2742
  });
2655
2743
  }
2656
- const passwordHash = await chunk7I5INVNR_cjs.AuthManager.hashPassword("sonicjs!");
2744
+ const passwordHash = await chunkT3YIKW2A_cjs.AuthManager.hashPassword("sonicjs!");
2657
2745
  const userId = "admin-user-id";
2658
2746
  const now = Date.now();
2659
2747
  const adminEmail = "admin@sonicjs.com".toLowerCase();
@@ -2673,7 +2761,6 @@ authRoutes.post("/seed-admin", async (c) => {
2673
2761
  now,
2674
2762
  now
2675
2763
  ).run();
2676
- setAdminExists();
2677
2764
  return c.json({
2678
2765
  message: "Admin user created successfully",
2679
2766
  user: {
@@ -2874,7 +2961,7 @@ authRoutes.post("/accept-invitation", async (c) => {
2874
2961
  if (existingUsername) {
2875
2962
  return c.json({ error: "Username is already taken" }, 400);
2876
2963
  }
2877
- const passwordHash = await chunk7I5INVNR_cjs.AuthManager.hashPassword(password);
2964
+ const passwordHash = await chunkT3YIKW2A_cjs.AuthManager.hashPassword(password);
2878
2965
  const updateStmt = db.prepare(`
2879
2966
  UPDATE users SET
2880
2967
  username = ?,
@@ -2893,7 +2980,7 @@ authRoutes.post("/accept-invitation", async (c) => {
2893
2980
  Date.now(),
2894
2981
  invitedUser.id
2895
2982
  ).run();
2896
- const authToken = await chunk7I5INVNR_cjs.AuthManager.generateToken(invitedUser.id, invitedUser.email, invitedUser.role);
2983
+ const authToken = await chunkT3YIKW2A_cjs.AuthManager.generateToken(invitedUser.id, invitedUser.email, invitedUser.role);
2897
2984
  cookie.setCookie(c, "auth_token", authToken, {
2898
2985
  httpOnly: true,
2899
2986
  secure: true,
@@ -3123,7 +3210,7 @@ authRoutes.post("/reset-password", async (c) => {
3123
3210
  if (Date.now() > user.password_reset_expires) {
3124
3211
  return c.json({ error: "Reset token has expired" }, 400);
3125
3212
  }
3126
- const newPasswordHash = await chunk7I5INVNR_cjs.AuthManager.hashPassword(password);
3213
+ const newPasswordHash = await chunkT3YIKW2A_cjs.AuthManager.hashPassword(password);
3127
3214
  try {
3128
3215
  const historyStmt = db.prepare(`
3129
3216
  INSERT INTO password_history (id, user_id, password_hash, created_at)
@@ -3381,11 +3468,168 @@ app.post("/test-cleanup/content", async (c) => {
3381
3468
  var test_cleanup_default = app;
3382
3469
 
3383
3470
  // src/templates/pages/admin-content-form.template.ts
3384
- chunkAZLU3ROK_cjs.init_admin_layout_catalyst_template();
3471
+ chunkBZC4FYW7_cjs.init_admin_layout_catalyst_template();
3472
+
3473
+ // src/templates/components/drag-sortable.template.ts
3474
+ function getDragSortableScript() {
3475
+ return `
3476
+ <script>
3477
+ if (!window.__sonicDragSortableInit) {
3478
+ window.__sonicDragSortableInit = true;
3479
+
3480
+ window.initializeDragSortable = function(container, options) {
3481
+ if (!container || container.dataset.dragSortableInit === 'true') {
3482
+ return;
3483
+ }
3484
+
3485
+ container.dataset.dragSortableInit = 'true';
3486
+ const itemSelector = options && options.itemSelector ? options.itemSelector : '.sortable-item';
3487
+ const handleSelector = options && options.handleSelector ? options.handleSelector : '[data-action="drag-handle"]';
3488
+ const onUpdate = options && typeof options.onUpdate === 'function' ? options.onUpdate : function() {};
3489
+ let activeDragItem = null;
3490
+
3491
+ const getDragAfterElement = function(list, y) {
3492
+ const items = Array.from(list.querySelectorAll(itemSelector + ':not(.is-dragging)'));
3493
+ let closest = { offset: Number.NEGATIVE_INFINITY, element: null };
3494
+ items.forEach(function(item) {
3495
+ const box = item.getBoundingClientRect();
3496
+ const offset = y - box.top - box.height / 2;
3497
+ if (offset < 0 && offset > closest.offset) {
3498
+ closest = { offset: offset, element: item };
3499
+ }
3500
+ });
3501
+ return closest.element;
3502
+ };
3503
+
3504
+ const activateDragItem = function(event) {
3505
+ const target = event.target;
3506
+ if (!(target instanceof Element)) return;
3507
+ const handle = target.closest(handleSelector);
3508
+ if (!handle) return;
3509
+ const item = handle.closest(itemSelector);
3510
+ if (!item) return;
3511
+ activeDragItem = item;
3512
+ };
3513
+
3514
+ const clearActiveDragItem = function() {
3515
+ activeDragItem = null;
3516
+ };
3517
+
3518
+ container.addEventListener('pointerdown', activateDragItem);
3519
+ container.addEventListener('mousedown', activateDragItem);
3520
+ container.addEventListener('pointerup', clearActiveDragItem);
3521
+ container.addEventListener('mouseup', clearActiveDragItem);
3522
+
3523
+ container.addEventListener('dragstart', function(event) {
3524
+ const target = event.target;
3525
+ if (!(target instanceof Element)) return;
3526
+ const item = target.closest(itemSelector);
3527
+ if (!item || item !== activeDragItem) {
3528
+ event.preventDefault();
3529
+ return;
3530
+ }
3531
+ item.classList.add('is-dragging');
3532
+ if (event.dataTransfer) {
3533
+ event.dataTransfer.setData('text/plain', '');
3534
+ }
3535
+ });
3536
+
3537
+ container.addEventListener('dragend', function(event) {
3538
+ const target = event.target;
3539
+ if (target instanceof Element) {
3540
+ const item = target.closest(itemSelector);
3541
+ if (item) {
3542
+ item.classList.remove('is-dragging');
3543
+ }
3544
+ }
3545
+ activeDragItem = null;
3546
+ onUpdate();
3547
+ });
3548
+
3549
+ container.addEventListener('dragover', function(event) {
3550
+ event.preventDefault();
3551
+ const dragging = container.querySelector(itemSelector + '.is-dragging');
3552
+ if (!dragging) return;
3553
+ const afterElement = getDragAfterElement(container, event.clientY);
3554
+ if (afterElement === null) {
3555
+ container.appendChild(dragging);
3556
+ } else {
3557
+ container.insertBefore(dragging, afterElement);
3558
+ }
3559
+ });
3560
+
3561
+ container.addEventListener('drop', function() {
3562
+ onUpdate();
3563
+ });
3564
+ };
3565
+ }
3566
+ </script>
3567
+ `;
3568
+ }
3385
3569
 
3386
3570
  // src/templates/components/dynamic-field.template.ts
3571
+ function getReadFieldValueScript() {
3572
+ return `
3573
+ <script>
3574
+ if (!window.__sonicReadFieldValueInit) {
3575
+ window.__sonicReadFieldValueInit = true;
3576
+
3577
+ window.sonicReadFieldValue = function(fieldWrapper) {
3578
+ const fieldType = fieldWrapper.dataset.fieldType;
3579
+ const select = fieldWrapper.querySelector('select');
3580
+ const textarea = fieldWrapper.querySelector('textarea');
3581
+ const inputs = Array.from(fieldWrapper.querySelectorAll('input'));
3582
+ const checkbox = inputs.find((input) => input.type === 'checkbox');
3583
+ const nonHiddenInput = inputs.find((input) => input.type !== 'hidden' && input.type !== 'checkbox');
3584
+ const hiddenInput = inputs.find((input) => input.type === 'hidden');
3585
+
3586
+ if (fieldType === 'object' || fieldType === 'array') {
3587
+ if (!hiddenInput) {
3588
+ return fieldType === 'array' ? [] : {};
3589
+ }
3590
+ const rawValue = hiddenInput.value || '';
3591
+ if (!rawValue.trim()) {
3592
+ return fieldType === 'array' ? [] : {};
3593
+ }
3594
+ try {
3595
+ return JSON.parse(rawValue);
3596
+ } catch {
3597
+ return fieldType === 'array' ? [] : {};
3598
+ }
3599
+ }
3600
+
3601
+ if (fieldType === 'boolean' && checkbox) {
3602
+ return checkbox.checked;
3603
+ }
3604
+
3605
+ if (select) {
3606
+ if (select.multiple) {
3607
+ return Array.from(select.selectedOptions).map((option) => option.value);
3608
+ }
3609
+ return select.value;
3610
+ }
3611
+
3612
+ if (fieldType === 'quill' || fieldType === 'media') {
3613
+ return hiddenInput ? hiddenInput.value : '';
3614
+ }
3615
+
3616
+ const textSource = textarea || nonHiddenInput || hiddenInput;
3617
+ if (!textSource) {
3618
+ return '';
3619
+ }
3620
+
3621
+ if (fieldType === 'number') {
3622
+ return textSource.value === '' ? null : Number(textSource.value);
3623
+ }
3624
+
3625
+ return textSource.value;
3626
+ };
3627
+ }
3628
+ </script>
3629
+ `;
3630
+ }
3387
3631
  function renderDynamicField(field, options = {}) {
3388
- const { value = "", errors = [], disabled = false, className = "", pluginStatuses = {} } = options;
3632
+ const { value = "", errors = [], disabled = false, className = "", pluginStatuses = {}, collectionId = "", contentId = "" } = options;
3389
3633
  const opts = field.field_options || {};
3390
3634
  const required = field.is_required ? "required" : "";
3391
3635
  const baseClasses = `w-full rounded-lg px-3 py-2 text-sm text-zinc-950 dark:text-white bg-white dark:bg-zinc-800 shadow-sm ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 placeholder:text-zinc-400 dark:placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-zinc-950 dark:focus:ring-white transition-shadow ${className}`;
@@ -3638,67 +3882,171 @@ function renderDynamicField(field, options = {}) {
3638
3882
  `;
3639
3883
  break;
3640
3884
  case "slug":
3641
- let slugPattern = opts.pattern || "^[a-z0-9-]+$";
3642
- let slugHelp = '<p class="mt-2 text-xs text-zinc-500 dark:text-zinc-400">Use lowercase letters, numbers, and hyphens only</p>';
3643
- slugHelp += `<button type="button" class="mt-1 text-xs text-cyan-600 dark:text-cyan-400 hover:text-cyan-700 dark:hover:text-cyan-300" onclick="generateSlugFromTitle('\${fieldId}')">Generate from title</button>`;
3885
+ const slugPattern = opts.pattern || "^[a-z0-9-]+$";
3886
+ const collectionIdValue = collectionId || opts.collectionId || "";
3887
+ const contentIdValue = contentId || opts.contentId || "";
3888
+ const isEditMode = !!value;
3644
3889
  fieldHTML = `
3645
- <input
3646
- type="text"
3647
- id="${fieldId}"
3648
- name="${fieldName}"
3649
- value="${escapeHtml2(value)}"
3650
- placeholder="${opts.placeholder || "url-friendly-slug"}"
3651
- maxlength="${opts.maxLength || ""}"
3652
- data-pattern="${slugPattern}"
3653
- class="${baseClasses} ${errorClasses}"
3654
- ${required}
3655
- ${disabled ? "disabled" : ""}
3656
- >
3657
- ${slugHelp}
3890
+ <div class="slug-field-container">
3891
+ <input
3892
+ type="text"
3893
+ id="${fieldId}"
3894
+ name="${fieldName}"
3895
+ value="${escapeHtml2(value)}"
3896
+ placeholder="${opts.placeholder || "url-friendly-slug"}"
3897
+ maxlength="${opts.maxLength || 100}"
3898
+ data-pattern="${slugPattern}"
3899
+ data-collection-id="${collectionIdValue}"
3900
+ data-content-id="${contentIdValue}"
3901
+ data-is-edit-mode="${isEditMode}"
3902
+ class="${baseClasses} ${errorClasses}"
3903
+ ${required}
3904
+ ${disabled ? "disabled" : ""}
3905
+ >
3906
+ <div id="${fieldId}-status" class="slug-status mt-1 text-sm min-h-[20px]"></div>
3907
+ <button
3908
+ type="button"
3909
+ class="regenerate-slug-btn mt-2 text-sm text-cyan-600 dark:text-cyan-400 hover:text-cyan-700 dark:hover:text-cyan-300 flex items-center gap-1 transition-colors"
3910
+ onclick="window.regenerateSlugFromTitle_${fieldId.replace(/-/g, "_")}()"
3911
+ >
3912
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
3913
+ <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"></path>
3914
+ </svg>
3915
+ Regenerate from title
3916
+ </button>
3917
+ <p class="text-xs text-zinc-500 dark:text-zinc-400 mt-1">Use lowercase letters, numbers, and hyphens only</p>
3918
+ </div>
3919
+
3658
3920
  <script>
3659
3921
  (function() {
3660
- const field = document.getElementById('${fieldId}');
3922
+ const slugField = document.getElementById('${fieldId}');
3923
+ const statusDiv = document.getElementById('${fieldId}-status');
3924
+ const isEditMode = slugField.dataset.isEditMode === 'true';
3661
3925
  const pattern = new RegExp('${slugPattern}');
3662
-
3663
- field.addEventListener('input', function() {
3664
- if (this.value && !pattern.test(this.value)) {
3665
- this.setCustomValidity('Please use only lowercase letters, numbers, and hyphens.');
3666
- } else {
3667
- this.setCustomValidity('');
3668
- }
3669
- });
3670
-
3671
- field.addEventListener('blur', function() {
3672
- this.reportValidity();
3673
- });
3674
- })();
3675
-
3676
- function generateSlugFromTitle(slugFieldId) {
3677
- const titleField = document.querySelector('input[name="title"]');
3678
- const slugField = document.getElementById(slugFieldId);
3679
- if (titleField && slugField) {
3680
- const slug = titleField.value
3926
+ const collectionId = slugField.dataset.collectionId;
3927
+ const contentId = slugField.dataset.contentId;
3928
+
3929
+ let checkTimeout;
3930
+ let lastCheckedSlug = '';
3931
+ let manuallyEdited = false;
3932
+
3933
+ // Shared slug generation function
3934
+ function generateSlug(text) {
3935
+ if (!text) return '';
3936
+
3937
+ return text
3681
3938
  .toLowerCase()
3939
+ .normalize('NFD')
3940
+ .replace(/[\\u0300-\\u036f]/g, '')
3682
3941
  .replace(/[^a-z0-9\\s_-]/g, '')
3683
3942
  .replace(/\\s+/g, '-')
3684
3943
  .replace(/[-_]+/g, '-')
3685
- .replace(/^[-_]|[-_]$/g, '');
3686
- slugField.value = slug;
3944
+ .replace(/^[-_]+|[-_]+$/g, '')
3945
+ .substring(0, 100);
3687
3946
  }
3688
- }
3689
-
3690
- // Auto-generate slug when title changes
3691
- document.addEventListener('DOMContentLoaded', function() {
3692
- const titleField = document.querySelector('input[name="title"]');
3693
- const slugField = document.getElementById('${fieldId}');
3694
- if (titleField && slugField && !slugField.value) {
3695
- titleField.addEventListener('input', function() {
3696
- if (!slugField.value) {
3697
- generateSlugFromTitle('${fieldId}');
3947
+
3948
+ // Check if slug is available
3949
+ async function checkSlugAvailability(slug) {
3950
+ if (!slug || !collectionId) return;
3951
+
3952
+ // Don't check if it's the same as last time
3953
+ if (slug === lastCheckedSlug) return;
3954
+ lastCheckedSlug = slug;
3955
+
3956
+ try {
3957
+ // Show checking status
3958
+ statusDiv.innerHTML = '<span class="text-gray-400">\u23F3 Checking availability...</span>';
3959
+
3960
+ // Build URL
3961
+ let url = \`/api/content/check-slug?collectionId=\${encodeURIComponent(collectionId)}&slug=\${encodeURIComponent(slug)}\`;
3962
+ if (contentId) {
3963
+ url += \`&excludeId=\${encodeURIComponent(contentId)}\`;
3698
3964
  }
3699
- });
3965
+
3966
+ const response = await fetch(url);
3967
+ const data = await response.json();
3968
+
3969
+ if (data.available) {
3970
+ statusDiv.innerHTML = '<span class="text-green-500 dark:text-green-400">\u2713 Available</span>';
3971
+ slugField.setCustomValidity('');
3972
+ } else {
3973
+ statusDiv.innerHTML = \`<span class="text-red-500 dark:text-red-400">\u2717 \${data.message || 'Already in use'}</span>\`;
3974
+ slugField.setCustomValidity(data.message || 'This slug is already in use');
3975
+ }
3976
+ } catch (error) {
3977
+ console.error('Error checking slug:', error);
3978
+ statusDiv.innerHTML = '<span class="text-yellow-500 dark:text-yellow-400">\u26A0 Could not verify</span>';
3979
+ }
3700
3980
  }
3701
- });
3981
+
3982
+ // Format validation and duplicate checking
3983
+ slugField.addEventListener('input', function() {
3984
+ const value = this.value;
3985
+
3986
+ // Mark as manually edited if user types directly
3987
+ if (document.activeElement === this) {
3988
+ manuallyEdited = true;
3989
+ }
3990
+
3991
+ // Clear status if empty
3992
+ if (!value) {
3993
+ statusDiv.innerHTML = '';
3994
+ this.setCustomValidity('');
3995
+ return;
3996
+ }
3997
+
3998
+ // Pattern validation
3999
+ if (!pattern.test(value)) {
4000
+ this.setCustomValidity('Please use only lowercase letters, numbers, and hyphens.');
4001
+ statusDiv.innerHTML = '<span class="text-red-500 dark:text-red-400">\u2717 Invalid format</span>';
4002
+ return;
4003
+ }
4004
+
4005
+ // Debounce the availability check
4006
+ clearTimeout(checkTimeout);
4007
+ checkTimeout = setTimeout(() => {
4008
+ checkSlugAvailability(value);
4009
+ }, 500); // Wait 500ms after user stops typing
4010
+ });
4011
+
4012
+ // Initial check if field has value
4013
+ if (slugField.value) {
4014
+ checkSlugAvailability(slugField.value);
4015
+ }
4016
+
4017
+ // Auto-generate only in create mode
4018
+ // Wait for all fields to be rendered before attaching listeners
4019
+ if (!isEditMode) {
4020
+ // Use setTimeout to ensure all fields in the form are rendered
4021
+ setTimeout(() => {
4022
+ const titleField = document.querySelector('input[name="title"]');
4023
+ if (titleField) {
4024
+ titleField.addEventListener('input', function() {
4025
+ if (!manuallyEdited) {
4026
+ const slug = generateSlug(this.value);
4027
+ slugField.value = slug;
4028
+
4029
+ // Trigger validation and duplicate check
4030
+ slugField.dispatchEvent(new Event('input', { bubbles: true }));
4031
+ }
4032
+ });
4033
+ }
4034
+ }, 0);
4035
+ }
4036
+
4037
+ // Global function for regenerate button
4038
+ window.regenerateSlugFromTitle_${fieldId.replace(/-/g, "_")} = function() {
4039
+ const titleField = document.querySelector('input[name="title"]');
4040
+ if (titleField && slugField) {
4041
+ const slug = generateSlug(titleField.value);
4042
+ slugField.value = slug;
4043
+ manuallyEdited = false;
4044
+
4045
+ // Trigger validation and duplicate check
4046
+ slugField.dispatchEvent(new Event('input', { bubbles: true }));
4047
+ }
4048
+ };
4049
+ })();
3702
4050
  </script>
3703
4051
  `;
3704
4052
  break;
@@ -3735,43 +4083,124 @@ function renderDynamicField(field, options = {}) {
3735
4083
  ` : ""}
3736
4084
  `;
3737
4085
  break;
4086
+ case "reference":
4087
+ let referenceCollections = [];
4088
+ if (Array.isArray(opts.collection)) {
4089
+ referenceCollections = opts.collection.filter(Boolean);
4090
+ } else if (typeof opts.collection === "string" && opts.collection) {
4091
+ referenceCollections = [opts.collection];
4092
+ }
4093
+ const referenceCollectionsAttr = referenceCollections.join(",");
4094
+ const hasReferenceCollection = referenceCollections.length > 0;
4095
+ const hasReferenceValue = Boolean(value);
4096
+ fieldHTML = `
4097
+ <div class="reference-field-container space-y-3" data-reference-field data-field-name="${escapeHtml2(fieldName)}" data-reference-collection="${escapeHtml2(referenceCollections[0] || "")}" data-reference-collections="${escapeHtml2(referenceCollectionsAttr)}">
4098
+ <input type="hidden" id="${fieldId}" name="${fieldName}" value="${escapeHtml2(value)}">
4099
+ <div class="rounded-lg border border-zinc-200 bg-white/60 px-3 py-2 text-sm text-zinc-600 dark:border-white/10 dark:bg-white/5 dark:text-zinc-300" data-reference-display>
4100
+ ${hasReferenceCollection ? hasReferenceValue ? "Loading selection..." : "No reference selected." : "Reference collection not configured."}
4101
+ </div>
4102
+ <div class="flex flex-wrap gap-2">
4103
+ <button
4104
+ type="button"
4105
+ onclick="openReferenceSelector('${fieldId}')"
4106
+ class="inline-flex items-center justify-center rounded-lg bg-zinc-900 px-3 py-2 text-sm font-semibold text-white hover:bg-zinc-800 dark:bg-white/10 dark:hover:bg-white/20"
4107
+ ${hasReferenceCollection ? "" : "disabled"}
4108
+ >
4109
+ Select reference
4110
+ </button>
4111
+ <button
4112
+ type="button"
4113
+ onclick="clearReferenceField('${fieldId}')"
4114
+ class="inline-flex items-center justify-center rounded-lg border border-zinc-200 px-3 py-2 text-sm font-semibold text-zinc-700 hover:bg-zinc-100 dark:border-white/10 dark:text-zinc-200 dark:hover:bg-white/10"
4115
+ data-reference-clear
4116
+ ${hasReferenceValue ? "" : "disabled"}
4117
+ >
4118
+ Remove
4119
+ </button>
4120
+ </div>
4121
+ </div>
4122
+ `;
4123
+ break;
3738
4124
  case "media":
4125
+ const isMultiple = opts.multiple === true;
4126
+ const mediaValues = isMultiple && value ? Array.isArray(value) ? value : String(value).split(",").filter(Boolean) : [];
4127
+ const singleValue = !isMultiple ? value : "";
4128
+ const isVideoUrl = (url) => {
4129
+ const videoExtensions = [".mp4", ".webm", ".ogg", ".mov", ".avi"];
4130
+ return videoExtensions.some((ext) => url.toLowerCase().endsWith(ext));
4131
+ };
4132
+ const renderMediaPreview = (url, alt, classes) => {
4133
+ if (isVideoUrl(url)) {
4134
+ return `<video src="${url}" class="${classes}" muted></video>`;
4135
+ }
4136
+ return `<img src="${url}" alt="${alt}" class="${classes}">`;
4137
+ };
3739
4138
  fieldHTML = `
3740
4139
  <div class="media-field-container">
3741
- <input type="hidden" id="${fieldId}" name="${fieldName}" value="${value}">
3742
- <div class="media-preview ${value ? "" : "hidden"}" id="${fieldId}-preview">
3743
- ${value ? `<img src="${value}" alt="Selected media" class="w-32 h-32 object-cover rounded-lg border border-white/20">` : ""}
3744
- </div>
4140
+ <input type="hidden" id="${fieldId}" name="${fieldName}" value="${isMultiple ? mediaValues.join(",") : singleValue}" data-multiple="${isMultiple}">
4141
+
4142
+ ${isMultiple ? `
4143
+ <div class="media-preview-grid grid grid-cols-4 gap-2 mb-2 ${mediaValues.length === 0 ? "hidden" : ""}" id="${fieldId}-preview">
4144
+ ${mediaValues.map((url, idx) => `
4145
+ <div class="relative media-preview-item" data-url="${url}">
4146
+ ${renderMediaPreview(url, `Media ${idx + 1}`, "w-full h-24 object-cover rounded-lg border border-white/20")}
4147
+ <button
4148
+ type="button"
4149
+ onclick="removeMediaFromMultiple('${fieldId}', '${url}')"
4150
+ class="absolute top-1 right-1 bg-red-600 text-white rounded-full p-1 hover:bg-red-700"
4151
+ ${disabled ? "disabled" : ""}
4152
+ >
4153
+ <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
4154
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
4155
+ </svg>
4156
+ </button>
4157
+ </div>
4158
+ `).join("")}
4159
+ </div>
4160
+ ` : `
4161
+ <div class="media-preview ${singleValue ? "" : "hidden"}" id="${fieldId}-preview">
4162
+ ${singleValue ? renderMediaPreview(singleValue, "Selected media", "w-32 h-32 object-cover rounded-lg border border-white/20") : ""}
4163
+ </div>
4164
+ `}
4165
+
3745
4166
  <div class="media-actions mt-2 space-x-2">
3746
4167
  <button
3747
4168
  type="button"
3748
- onclick="openMediaSelector('${fieldId}')"
4169
+ onclick="openMediaSelector('${fieldId}', ${isMultiple})"
3749
4170
  class="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-xl hover:bg-blue-700 transition-all"
3750
4171
  ${disabled ? "disabled" : ""}
3751
4172
  >
3752
4173
  <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
3753
4174
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
3754
4175
  </svg>
3755
- Select Media
4176
+ ${isMultiple ? "Select Media (Multiple)" : "Select Media"}
3756
4177
  </button>
3757
- ${value ? `
4178
+ ${(isMultiple ? mediaValues.length > 0 : singleValue) ? `
3758
4179
  <button
3759
4180
  type="button"
3760
4181
  onclick="clearMediaField('${fieldId}')"
3761
4182
  class="inline-flex items-center px-4 py-2 bg-red-600 text-white rounded-xl hover:bg-red-700 transition-all"
3762
4183
  ${disabled ? "disabled" : ""}
3763
4184
  >
3764
- Remove
4185
+ ${isMultiple ? "Clear All" : "Remove"}
3765
4186
  </button>
3766
4187
  ` : ""}
3767
4188
  </div>
3768
4189
  </div>
3769
4190
  `;
3770
4191
  break;
4192
+ case "object":
4193
+ return renderStructuredObjectField(field, options2);
4194
+ case "array":
4195
+ const itemsConfig = opts.items && typeof opts.items === "object" ? opts.items : {};
4196
+ if (itemsConfig.blocks && typeof itemsConfig.blocks === "object") {
4197
+ return renderBlocksField(field, options2, baseClasses, errorClasses);
4198
+ }
4199
+ return renderStructuredArrayField(field, options2);
3771
4200
  default:
3772
4201
  fieldHTML = `
3773
- <input
3774
- type="text"
4202
+ <input
4203
+ type="text"
3775
4204
  id="${fieldId}"
3776
4205
  name="${fieldName}"
3777
4206
  value="${escapeHtml2(value)}"
@@ -3821,220 +4250,767 @@ function renderFieldGroup(title, fields, collapsible = false) {
3821
4250
  </div>
3822
4251
  `;
3823
4252
  }
3824
- function escapeHtml2(text) {
3825
- if (typeof text !== "string") return String(text || "");
3826
- return text.replace(/[&<>"']/g, (char) => ({
3827
- "&": "&amp;",
3828
- "<": "&lt;",
3829
- ">": "&gt;",
3830
- '"': "&quot;",
3831
- "'": "&#39;"
3832
- })[char] || char);
3833
- }
3834
- var PluginBuilder = class _PluginBuilder {
3835
- plugin;
3836
- constructor(options) {
3837
- this.plugin = {
3838
- name: options.name,
3839
- version: options.version,
3840
- description: options.description,
3841
- author: options.author,
3842
- dependencies: options.dependencies,
3843
- routes: [],
3844
- middleware: [],
3845
- models: [],
3846
- services: [],
3847
- adminPages: [],
3848
- adminComponents: [],
3849
- menuItems: [],
3850
- hooks: []
3851
- };
3852
- }
3853
- /**
3854
- * Create a new plugin builder
3855
- */
3856
- static create(options) {
3857
- return new _PluginBuilder(options);
3858
- }
3859
- /**
3860
- * Add metadata to the plugin
3861
- */
3862
- metadata(metadata) {
3863
- Object.assign(this.plugin, metadata);
3864
- return this;
3865
- }
3866
- /**
3867
- * Add routes to plugin
3868
- */
3869
- addRoutes(routes) {
3870
- this.plugin.routes = [...this.plugin.routes || [], ...routes];
3871
- return this;
3872
- }
3873
- /**
3874
- * Add a single route to plugin
3875
- */
3876
- addRoute(path, handler, options) {
3877
- const route = {
3878
- path,
3879
- handler,
3880
- ...options
3881
- };
3882
- this.plugin.routes = [...this.plugin.routes || [], route];
3883
- return this;
3884
- }
3885
- /**
3886
- * Add middleware to plugin
3887
- */
3888
- addMiddleware(middleware) {
3889
- this.plugin.middleware = [...this.plugin.middleware || [], ...middleware];
3890
- return this;
3891
- }
3892
- /**
3893
- * Add a single middleware to plugin
3894
- */
3895
- addSingleMiddleware(name, handler, options) {
3896
- const middleware = {
3897
- name,
3898
- handler,
3899
- ...options
3900
- };
3901
- this.plugin.middleware = [...this.plugin.middleware || [], middleware];
3902
- return this;
3903
- }
3904
- /**
3905
- * Add models to plugin
3906
- */
3907
- addModels(models) {
3908
- this.plugin.models = [...this.plugin.models || [], ...models];
3909
- return this;
3910
- }
3911
- /**
3912
- * Add a single model to plugin
3913
- */
3914
- addModel(name, options) {
3915
- const model = {
3916
- name,
3917
- ...options
3918
- };
3919
- this.plugin.models = [...this.plugin.models || [], model];
3920
- return this;
3921
- }
3922
- /**
3923
- * Add services to plugin
3924
- */
3925
- addServices(services) {
3926
- this.plugin.services = [...this.plugin.services || [], ...services];
3927
- return this;
3928
- }
3929
- /**
3930
- * Add a single service to plugin
3931
- */
3932
- addService(name, implementation, options) {
3933
- const service = {
3934
- name,
3935
- implementation,
3936
- ...options
3937
- };
3938
- this.plugin.services = [...this.plugin.services || [], service];
3939
- return this;
3940
- }
3941
- /**
3942
- * Add admin pages to plugin
3943
- */
3944
- addAdminPages(pages) {
3945
- this.plugin.adminPages = [...this.plugin.adminPages || [], ...pages];
3946
- return this;
3947
- }
3948
- /**
3949
- * Add a single admin page to plugin
3950
- */
3951
- addAdminPage(path, title, component, options) {
3952
- const page = {
3953
- path,
3954
- title,
3955
- component,
3956
- ...options
3957
- };
3958
- this.plugin.adminPages = [...this.plugin.adminPages || [], page];
3959
- return this;
3960
- }
3961
- /**
3962
- * Add admin components to plugin
3963
- */
3964
- addComponents(components) {
3965
- this.plugin.adminComponents = [...this.plugin.adminComponents || [], ...components];
3966
- return this;
3967
- }
3968
- /**
3969
- * Add a single admin component to plugin
3970
- */
3971
- addComponent(name, template, options) {
3972
- const component = {
3973
- name,
3974
- template,
3975
- ...options
3976
- };
3977
- this.plugin.adminComponents = [...this.plugin.adminComponents || [], component];
3978
- return this;
3979
- }
3980
- /**
3981
- * Add menu items to plugin
3982
- */
3983
- addMenuItems(items) {
3984
- this.plugin.menuItems = [...this.plugin.menuItems || [], ...items];
3985
- return this;
4253
+ function renderBlocksField(field, options, baseClasses, errorClasses) {
4254
+ const { value = [], pluginStatuses = {} } = options;
4255
+ const opts = field.field_options || {};
4256
+ const itemsConfig = opts.items && typeof opts.items === "object" ? opts.items : {};
4257
+ const blocks = normalizeBlockDefinitions(itemsConfig.blocks);
4258
+ const discriminator = typeof itemsConfig.discriminator === "string" && itemsConfig.discriminator ? itemsConfig.discriminator : "blockType";
4259
+ const blockValues = normalizeBlocksValue(value, discriminator);
4260
+ const fieldId = `field-${field.field_name}`;
4261
+ const fieldName = field.field_name;
4262
+ const emptyState = blockValues.length === 0 ? `
4263
+ <div class="rounded-lg border border-dashed border-zinc-200 dark:border-white/10 px-4 py-6 text-center text-sm text-zinc-500 dark:text-zinc-400" data-blocks-empty>
4264
+ No blocks yet. Add your first block to get started.
4265
+ </div>
4266
+ ` : "";
4267
+ const blockOptions = blocks.map((block) => `<option value="${escapeHtml2(block.name)}">${escapeHtml2(block.label)}</option>`).join("");
4268
+ const blockItems = blockValues.map(
4269
+ (blockValue, index) => renderBlockItem(field, blockValue, blocks, discriminator, index, pluginStatuses)
4270
+ ).join("");
4271
+ const templates = blocks.map((block) => renderBlockTemplate(field, block, discriminator, pluginStatuses)).join("");
4272
+ return `
4273
+ <div
4274
+ class="blocks-field space-y-4"
4275
+ data-blocks='${escapeHtml2(JSON.stringify(blocks))}'
4276
+ data-blocks-discriminator="${escapeHtml2(discriminator)}"
4277
+ data-field-name="${escapeHtml2(fieldName)}"
4278
+ >
4279
+ <input type="hidden" id="${fieldId}" name="${fieldName}" value="${escapeHtml2(JSON.stringify(blockValues))}">
4280
+
4281
+ <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
4282
+ <div class="flex-1">
4283
+ <select
4284
+ class="${baseClasses} ${errorClasses}"
4285
+ data-role="block-type-select"
4286
+ >
4287
+ <option value="">Choose a block...</option>
4288
+ ${blockOptions}
4289
+ </select>
4290
+ </div>
4291
+ <button
4292
+ type="button"
4293
+ data-action="add-block"
4294
+ class="inline-flex items-center justify-center rounded-lg bg-zinc-900 px-4 py-2 text-sm font-semibold text-white hover:bg-zinc-800 dark:bg-white/10 dark:hover:bg-white/20"
4295
+ >
4296
+ Add Block
4297
+ </button>
4298
+ </div>
4299
+
4300
+ <div class="space-y-4" data-blocks-list>
4301
+ ${blockItems || emptyState}
4302
+ </div>
4303
+
4304
+ ${templates}
4305
+ </div>
4306
+ ${getDragSortableScript()}
4307
+ ${getBlocksFieldScript()}
4308
+ `;
4309
+ }
4310
+ function renderStructuredObjectField(field, options, baseClasses, errorClasses) {
4311
+ const { value = {}, pluginStatuses = {} } = options;
4312
+ const opts = field.field_options || {};
4313
+ const properties = opts.properties && typeof opts.properties === "object" ? opts.properties : {};
4314
+ const fieldId = `field-${field.field_name}`;
4315
+ const fieldName = field.field_name;
4316
+ const objectValue = normalizeStructuredObjectValue(value);
4317
+ const subfields = Object.entries(properties).map(
4318
+ ([propertyName, propertyConfig]) => renderStructuredSubfield(
4319
+ field,
4320
+ propertyName,
4321
+ propertyConfig,
4322
+ objectValue,
4323
+ pluginStatuses,
4324
+ field.field_name
4325
+ )
4326
+ ).join("");
4327
+ return `
4328
+ <div class="space-y-4" data-structured-object data-field-name="${escapeHtml2(fieldName)}">
4329
+ <input type="hidden" id="${fieldId}" name="${fieldName}" value="${escapeHtml2(JSON.stringify(objectValue))}">
4330
+ <div class="space-y-4" data-structured-object-fields>
4331
+ ${subfields}
4332
+ </div>
4333
+ </div>
4334
+ ${getStructuredFieldScript()}
4335
+ `;
4336
+ }
4337
+ function renderStructuredArrayField(field, options, baseClasses, errorClasses) {
4338
+ const { value = [], pluginStatuses = {} } = options;
4339
+ const opts = field.field_options || {};
4340
+ const itemsConfig = opts.items && typeof opts.items === "object" ? opts.items : {};
4341
+ const fieldId = `field-${field.field_name}`;
4342
+ const fieldName = field.field_name;
4343
+ const arrayValue = normalizeStructuredArrayValue(value);
4344
+ const items = arrayValue.map(
4345
+ (itemValue, index) => renderStructuredArrayItem(field, itemsConfig, String(index), itemValue, pluginStatuses)
4346
+ ).join("");
4347
+ const emptyState = arrayValue.length === 0 ? `
4348
+ <div class="rounded-lg border border-dashed border-zinc-200 dark:border-white/10 px-4 py-6 text-center text-sm text-zinc-500 dark:text-zinc-400" data-structured-empty>
4349
+ No items yet. Add the first item to get started.
4350
+ </div>
4351
+ ` : "";
4352
+ return `
4353
+ <div class="space-y-4" data-structured-array data-field-name="${escapeHtml2(fieldName)}">
4354
+ <input type="hidden" id="${fieldId}" name="${fieldName}" value="${escapeHtml2(JSON.stringify(arrayValue))}">
4355
+
4356
+ <div class="flex items-center justify-between gap-3">
4357
+ <div class="text-sm text-zinc-500 dark:text-zinc-400">
4358
+ ${escapeHtml2(opts.itemLabel || "Items")}
4359
+ </div>
4360
+ <button
4361
+ type="button"
4362
+ data-action="add-item"
4363
+ class="inline-flex items-center justify-center rounded-lg bg-zinc-900 px-3 py-2 text-sm font-semibold text-white hover:bg-zinc-800 dark:bg-white/10 dark:hover:bg-white/20"
4364
+ >
4365
+ Add item
4366
+ </button>
4367
+ </div>
4368
+
4369
+ <div class="space-y-4" data-structured-array-list>
4370
+ ${items || emptyState}
4371
+ </div>
4372
+
4373
+ <template data-structured-array-template>
4374
+ ${renderStructuredArrayItem(field, itemsConfig, "__INDEX__", {}, pluginStatuses)}
4375
+ </template>
4376
+ </div>
4377
+ ${getDragSortableScript()}
4378
+ ${getStructuredFieldScript()}
4379
+ `;
4380
+ }
4381
+ function renderStructuredArrayItem(field, itemConfig, index, itemValue, pluginStatuses) {
4382
+ const itemFields = renderStructuredItemFields(field, itemConfig, index, itemValue, pluginStatuses);
4383
+ return `
4384
+ <div class="structured-array-item rounded-lg border border-zinc-200 dark:border-white/10 bg-white/60 dark:bg-white/5 p-4 shadow-sm" data-array-index="${escapeHtml2(index)}" draggable="true">
4385
+ <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
4386
+ <div class="flex items-center gap-3">
4387
+ <div class="drag-handle cursor-move text-zinc-400 dark:text-zinc-500 hover:text-zinc-600 dark:hover:text-zinc-400" data-action="drag-handle" title="Drag to reorder">
4388
+ <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
4389
+ <path stroke-linecap="round" stroke-linejoin="round" d="M4 8h16M4 16h16"/>
4390
+ </svg>
4391
+ </div>
4392
+ <div class="text-sm font-semibold text-zinc-900 dark:text-white">
4393
+ Item <span class="ml-2 text-xs font-normal text-zinc-500 dark:text-zinc-400" data-array-order-label></span>
4394
+ </div>
4395
+ </div>
4396
+ <div class="flex flex-wrap gap-2 text-xs">
4397
+ <button type="button" data-action="move-up" class="inline-flex items-center justify-center rounded-md border border-zinc-200 px-2 py-1 text-zinc-600 hover:bg-zinc-100 dark:border-white/10 dark:text-zinc-300 dark:hover:bg-white/10 disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:bg-transparent dark:disabled:hover:bg-transparent" aria-label="Move item up" title="Move up">
4398
+ <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="4">
4399
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 6l-4 4m4-4l4 4m-4-4v12"/>
4400
+ </svg>
4401
+ </button>
4402
+ <button type="button" data-action="move-down" class="inline-flex items-center justify-center rounded-md border border-zinc-200 px-2 py-1 text-zinc-600 hover:bg-zinc-100 dark:border-white/10 dark:text-zinc-300 dark:hover:bg-white/10 disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:bg-transparent dark:disabled:hover:bg-transparent" aria-label="Move item down" title="Move down">
4403
+ <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="4">
4404
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 18l4-4m-4 4l-4-4m4 4V6"/>
4405
+ </svg>
4406
+ </button>
4407
+ <button type="button" data-action="remove-item" class="inline-flex items-center gap-x-1 px-2.5 py-1.5 text-xs font-medium text-pink-700 dark:text-pink-300 hover:bg-pink-50 dark:hover:bg-pink-900/20 rounded-lg transition-colors">
4408
+ <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
4409
+ <path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 0 00-7.5 0"/>
4410
+ </svg>
4411
+ Delete item
4412
+ </button>
4413
+ </div>
4414
+ </div>
4415
+ <div class="mt-4 space-y-4" data-array-item-fields>
4416
+ ${itemFields}
4417
+ </div>
4418
+ </div>
4419
+ `;
4420
+ }
4421
+ function renderStructuredItemFields(field, itemConfig, index, itemValue, pluginStatuses) {
4422
+ const itemType = itemConfig?.type || "string";
4423
+ if (itemType === "object" && itemConfig?.properties && typeof itemConfig.properties === "object") {
4424
+ const fieldPrefix = `array-${field.field_name}-${index}`;
4425
+ return Object.entries(itemConfig.properties).map(
4426
+ ([propertyName, propertyConfig]) => renderStructuredSubfield(
4427
+ field,
4428
+ propertyName,
4429
+ propertyConfig,
4430
+ itemValue || {},
4431
+ pluginStatuses,
4432
+ fieldPrefix
4433
+ )
4434
+ ).join("");
3986
4435
  }
3987
- /**
3988
- * Add a single menu item to plugin
3989
- */
3990
- addMenuItem(label, path, options) {
3991
- const menuItem = {
3992
- label,
3993
- path,
3994
- ...options
3995
- };
3996
- this.plugin.menuItems = [...this.plugin.menuItems || [], menuItem];
3997
- return this;
4436
+ const normalizedField = normalizeBlockField(itemConfig, "Item");
4437
+ const fieldValue = itemValue ?? normalizedField.defaultValue ?? "";
4438
+ const fieldDefinition = {
4439
+ id: `array-${field.field_name}-${index}-value`,
4440
+ field_name: `array-${field.field_name}-${index}-value`,
4441
+ field_type: normalizedField.type,
4442
+ field_label: normalizedField.label,
4443
+ field_options: normalizedField.options,
4444
+ is_required: normalizedField.required};
4445
+ return `
4446
+ <div class="structured-subfield" data-structured-field="__value" data-field-type="${escapeHtml2(normalizedField.type)}">
4447
+ ${renderDynamicField(fieldDefinition, { value: fieldValue, pluginStatuses })}
4448
+ </div>
4449
+ `;
4450
+ }
4451
+ function renderStructuredSubfield(field, propertyName, propertyConfig, objectValue, pluginStatuses, fieldPrefix) {
4452
+ const normalizedField = normalizeBlockField(propertyConfig, propertyName);
4453
+ const fieldValue = objectValue?.[propertyName] ?? normalizedField.defaultValue ?? "";
4454
+ const fieldDefinition = {
4455
+ field_name: `${fieldPrefix}__${propertyName}`,
4456
+ field_type: normalizedField.type,
4457
+ field_label: normalizedField.label,
4458
+ field_options: normalizedField.options,
4459
+ is_required: normalizedField.required};
4460
+ return `
4461
+ <div class="structured-subfield" data-structured-field="${escapeHtml2(propertyName)}" data-field-type="${escapeHtml2(normalizedField.type)}">
4462
+ ${renderDynamicField(fieldDefinition, { value: fieldValue, pluginStatuses })}
4463
+ </div>
4464
+ `;
4465
+ }
4466
+ function normalizeStructuredObjectValue(value) {
4467
+ if (!value) return {};
4468
+ if (typeof value === "string") {
4469
+ try {
4470
+ const parsed = JSON.parse(value);
4471
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
4472
+ } catch {
4473
+ return {};
4474
+ }
3998
4475
  }
3999
- /**
4000
- * Add hooks to plugin
4001
- */
4002
- addHooks(hooks) {
4003
- this.plugin.hooks = [...this.plugin.hooks || [], ...hooks];
4004
- return this;
4476
+ if (typeof value === "object" && !Array.isArray(value)) return value;
4477
+ return {};
4478
+ }
4479
+ function normalizeStructuredArrayValue(value) {
4480
+ if (!value) return [];
4481
+ if (Array.isArray(value)) return value;
4482
+ if (typeof value === "string") {
4483
+ try {
4484
+ const parsed = JSON.parse(value);
4485
+ return Array.isArray(parsed) ? parsed : [];
4486
+ } catch {
4487
+ return [];
4488
+ }
4005
4489
  }
4006
- /**
4007
- * Add a single hook to plugin
4008
- */
4009
- addHook(name, handler, options) {
4010
- const hook = {
4011
- name,
4012
- handler,
4013
- ...options
4014
- };
4015
- this.plugin.hooks = [...this.plugin.hooks || [], hook];
4016
- return this;
4490
+ return [];
4491
+ }
4492
+ function normalizeBlockDefinitions(rawBlocks) {
4493
+ if (!rawBlocks || typeof rawBlocks !== "object") return [];
4494
+ return Object.entries(rawBlocks).filter(([name, block]) => typeof name === "string" && block && typeof block === "object").map(([name, block]) => ({
4495
+ name,
4496
+ label: block.label || name,
4497
+ description: block.description,
4498
+ properties: block.properties && typeof block.properties === "object" ? block.properties : {}
4499
+ }));
4500
+ }
4501
+ function normalizeBlocksValue(value, discriminator) {
4502
+ const normalizeItem = (item) => {
4503
+ if (!item || typeof item !== "object") return null;
4504
+ if (item[discriminator]) return item;
4505
+ if (item.blockType && item.data && typeof item.data === "object") {
4506
+ return { [discriminator]: item.blockType, ...item.data };
4507
+ }
4508
+ return item;
4509
+ };
4510
+ const fromArray = (items) => items.map(normalizeItem).filter((item) => item && typeof item === "object");
4511
+ if (Array.isArray(value)) return fromArray(value);
4512
+ if (typeof value === "string" && value.trim()) {
4513
+ try {
4514
+ const parsed = JSON.parse(value);
4515
+ return Array.isArray(parsed) ? fromArray(parsed) : [];
4516
+ } catch {
4517
+ return [];
4518
+ }
4017
4519
  }
4018
- /**
4019
- * Add lifecycle hooks
4020
- */
4021
- lifecycle(hooks) {
4022
- Object.assign(this.plugin, hooks);
4023
- return this;
4520
+ return [];
4521
+ }
4522
+ function renderBlockTemplate(field, block, discriminator, pluginStatuses) {
4523
+ return `
4524
+ <template data-block-template="${escapeHtml2(block.name)}">
4525
+ ${renderBlockCard(field, block, discriminator, "__INDEX__", {}, pluginStatuses)}
4526
+ </template>
4527
+ `;
4528
+ }
4529
+ function renderBlockItem(field, blockValue, blocks, discriminator, index, pluginStatuses) {
4530
+ const blockType = blockValue?.[discriminator] || blockValue?.blockType;
4531
+ const blockDefinition = blocks.find((block) => block.name === blockType);
4532
+ if (!blockDefinition) {
4533
+ return `
4534
+ <div class="rounded-lg border border-amber-200 bg-amber-50/50 px-4 py-3 text-sm text-amber-700 dark:border-amber-500/30 dark:bg-amber-500/10 dark:text-amber-200" data-block-raw="${escapeHtml2(JSON.stringify(blockValue || {}))}">
4535
+ Unknown block type: <strong>${escapeHtml2(String(blockType || "unknown"))}</strong>. This block will be preserved as-is.
4536
+ </div>
4537
+ `;
4024
4538
  }
4025
- /**
4026
- * Build the plugin
4027
- */
4028
- build() {
4029
- if (!this.plugin.name || !this.plugin.version) {
4030
- throw new Error("Plugin name and version are required");
4539
+ const data = blockValue && typeof blockValue === "object" ? Object.fromEntries(Object.entries(blockValue).filter(([key]) => key !== discriminator)) : {};
4540
+ return renderBlockCard(field, blockDefinition, discriminator, String(index), data, pluginStatuses);
4541
+ }
4542
+ function renderBlockCard(field, block, discriminator, index, data, pluginStatuses) {
4543
+ const blockFields = Object.entries(block.properties).map(([fieldName, fieldConfig]) => {
4544
+ if (fieldConfig?.type === "array" && fieldConfig?.items?.blocks) {
4545
+ return `
4546
+ <div class="rounded-lg border border-dashed border-amber-200 bg-amber-50/50 px-4 py-3 text-xs text-amber-700 dark:border-amber-500/30 dark:bg-amber-500/10 dark:text-amber-200">
4547
+ Nested blocks are not supported yet for "${escapeHtml2(fieldName)}".
4548
+ </div>
4549
+ `;
4031
4550
  }
4032
- return this.plugin;
4551
+ const normalizedField = normalizeBlockField(fieldConfig, fieldName);
4552
+ const fieldValue = data?.[fieldName] ?? normalizedField.defaultValue ?? "";
4553
+ const fieldDefinition = {
4554
+ id: `block-${field.field_name}-${index}-${fieldName}`,
4555
+ field_name: `block-${field.field_name}-${index}-${fieldName}`,
4556
+ field_type: normalizedField.type,
4557
+ field_label: normalizedField.label,
4558
+ field_options: normalizedField.options,
4559
+ is_required: normalizedField.required};
4560
+ return `
4561
+ <div class="blocks-subfield" data-block-field="${escapeHtml2(fieldName)}" data-field-type="${escapeHtml2(normalizedField.type)}">
4562
+ ${renderDynamicField(fieldDefinition, { value: fieldValue, pluginStatuses })}
4563
+ </div>
4564
+ `;
4565
+ }).join("");
4566
+ return `
4567
+ <div class="blocks-item rounded-lg border border-zinc-200 dark:border-white/10 bg-white/60 dark:bg-white/5 p-4 shadow-sm" data-block-type="${escapeHtml2(block.name)}" data-block-discriminator="${escapeHtml2(discriminator)}" draggable="true">
4568
+ <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
4569
+ <div class="flex items-start gap-3">
4570
+ <div class="drag-handle cursor-move text-zinc-400 dark:text-zinc-500 hover:text-zinc-600 dark:hover:text-zinc-400" data-action="drag-handle" title="Drag to reorder">
4571
+ <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
4572
+ <path stroke-linecap="round" stroke-linejoin="round" d="M4 8h16M4 16h16"/>
4573
+ </svg>
4574
+ </div>
4575
+ <div>
4576
+ <div class="text-sm font-semibold text-zinc-900 dark:text-white">
4577
+ ${escapeHtml2(block.label)}
4578
+ <span class="ml-2 text-xs font-normal text-zinc-500 dark:text-zinc-400" data-block-order-label></span>
4579
+ </div>
4580
+ ${block.description ? `<p class="text-xs text-zinc-500 dark:text-zinc-400">${escapeHtml2(block.description)}</p>` : ""}
4581
+ </div>
4582
+ </div>
4583
+ <div class="flex flex-wrap gap-2 text-xs">
4584
+ <button type="button" data-action="move-up" class="inline-flex items-center justify-center rounded-md border border-zinc-200 px-2 py-1 text-zinc-600 hover:bg-zinc-100 dark:border-white/10 dark:text-zinc-300 dark:hover:bg-white/10 disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:bg-transparent dark:disabled:hover:bg-transparent" aria-label="Move block up" title="Move up">
4585
+ <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="4">
4586
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 6l-4 4m4-4l4 4m-4-4v12"/>
4587
+ </svg>
4588
+ </button>
4589
+ <button type="button" data-action="move-down" class="inline-flex items-center justify-center rounded-md border border-zinc-200 px-2 py-1 text-zinc-600 hover:bg-zinc-100 dark:border-white/10 dark:text-zinc-300 dark:hover:bg-white/10 disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:bg-transparent dark:disabled:hover:bg-transparent" aria-label="Move block down" title="Move down">
4590
+ <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="4">
4591
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 18l4-4m-4 4l-4-4m4 4V6"/>
4592
+ </svg>
4593
+ </button>
4594
+ <button type="button" data-action="remove-block" class="inline-flex items-center gap-x-1 px-2.5 py-1.5 text-xs font-medium text-pink-700 dark:text-pink-300 hover:bg-pink-50 dark:hover:bg-pink-900/20 rounded-lg transition-colors">
4595
+ <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
4596
+ <path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"/>
4597
+ </svg>
4598
+ Delete block
4599
+ </button>
4600
+ </div>
4601
+ </div>
4602
+ <div class="mt-4 space-y-4">
4603
+ ${blockFields}
4604
+ </div>
4605
+ </div>
4606
+ `;
4607
+ }
4608
+ function normalizeBlockField(fieldConfig, fieldName) {
4609
+ const type = fieldConfig?.type || "text";
4610
+ const label = fieldConfig?.title || fieldName;
4611
+ const required = fieldConfig?.required === true;
4612
+ const options = { ...fieldConfig };
4613
+ if (type === "select" && Array.isArray(fieldConfig?.enum)) {
4614
+ options.options = fieldConfig.enum.map((value, index) => ({
4615
+ value,
4616
+ label: fieldConfig.enumLabels?.[index] || value
4617
+ }));
4033
4618
  }
4034
- };
4619
+ return {
4620
+ type,
4621
+ label,
4622
+ required,
4623
+ defaultValue: fieldConfig?.default,
4624
+ options
4625
+ };
4626
+ }
4627
+ function getStructuredFieldScript() {
4628
+ return `
4629
+ ${getReadFieldValueScript()}
4630
+ <script>
4631
+ if (!window.__sonicStructuredFieldInit) {
4632
+ window.__sonicStructuredFieldInit = true;
4633
+
4634
+ function initializeStructuredFields() {
4635
+ const readFieldValue = window.sonicReadFieldValue;
4636
+
4637
+ const readStructuredValue = (container) => {
4638
+ const fields = Array.from(container.querySelectorAll('.structured-subfield'));
4639
+ if (fields.length === 1 && fields[0].dataset.structuredField === '__value') {
4640
+ return readFieldValue(fields[0]);
4641
+ }
4642
+
4643
+ return fields.reduce((acc, fieldWrapper) => {
4644
+ const fieldName = fieldWrapper.dataset.structuredField;
4645
+ if (!fieldName || fieldName === '__value') return acc;
4646
+ acc[fieldName] = readFieldValue(fieldWrapper);
4647
+ return acc;
4648
+ }, {});
4649
+ };
4650
+
4651
+ document.querySelectorAll('[data-structured-object]').forEach((container) => {
4652
+ if (container.dataset.structuredInitialized === 'true') {
4653
+ return;
4654
+ }
4655
+ container.dataset.structuredInitialized = 'true';
4656
+ const hiddenInput = container.querySelector('input[type="hidden"]');
4657
+
4658
+ const updateHiddenInput = () => {
4659
+ if (!hiddenInput) return;
4660
+ const value = readStructuredValue(container);
4661
+ hiddenInput.value = JSON.stringify(value);
4662
+ };
4663
+
4664
+ container.addEventListener('input', updateHiddenInput);
4665
+ container.addEventListener('change', updateHiddenInput);
4666
+ updateHiddenInput();
4667
+ });
4668
+
4669
+ document.querySelectorAll('[data-structured-array]').forEach((container) => {
4670
+ if (container.dataset.structuredInitialized === 'true') {
4671
+ return;
4672
+ }
4673
+ container.dataset.structuredInitialized = 'true';
4674
+ const list = container.querySelector('[data-structured-array-list]');
4675
+ const hiddenInput = container.querySelector('input[type="hidden"]');
4676
+ const template = container.querySelector('template[data-structured-array-template]');
4677
+
4678
+ const updateOrderLabels = () => {
4679
+ const items = Array.from(container.querySelectorAll('.structured-array-item'));
4680
+ items.forEach((item, index) => {
4681
+ const label = item.querySelector('[data-array-order-label]');
4682
+ if (label) {
4683
+ label.textContent = '#'+ (index + 1);
4684
+ }
4685
+
4686
+ const moveUpButton = item.querySelector('[data-action="move-up"]');
4687
+ if (moveUpButton instanceof HTMLButtonElement) {
4688
+ moveUpButton.disabled = index === 0;
4689
+ }
4690
+
4691
+ const moveDownButton = item.querySelector('[data-action="move-down"]');
4692
+ if (moveDownButton instanceof HTMLButtonElement) {
4693
+ moveDownButton.disabled = index === items.length - 1;
4694
+ }
4695
+ });
4696
+ };
4697
+
4698
+ const updateHiddenInput = () => {
4699
+ if (!hiddenInput || !list) return;
4700
+ const items = Array.from(list.querySelectorAll('.structured-array-item'));
4701
+ const values = items.map((item) => readStructuredValue(item));
4702
+ hiddenInput.value = JSON.stringify(values);
4703
+
4704
+ const emptyState = list.querySelector('[data-structured-empty]');
4705
+ if (emptyState) {
4706
+ emptyState.style.display = values.length === 0 ? 'block' : 'none';
4707
+ }
4708
+ updateOrderLabels();
4709
+ };
4710
+
4711
+ if (typeof window.initializeDragSortable === 'function' && list) {
4712
+ window.initializeDragSortable(list, {
4713
+ itemSelector: '.structured-array-item',
4714
+ handleSelector: '[data-action="drag-handle"]',
4715
+ onUpdate: updateHiddenInput
4716
+ });
4717
+ }
4718
+
4719
+ container.addEventListener('click', (event) => {
4720
+ const target = event.target;
4721
+ if (!(target instanceof Element)) return;
4722
+ const actionButton = target.closest('[data-action]');
4723
+ if (!actionButton || actionButton.hasAttribute('disabled')) return;
4724
+
4725
+ const action = actionButton.getAttribute('data-action');
4726
+
4727
+ if (action === 'add-item') {
4728
+ if (!list || !template) return;
4729
+ const nextIndex = list.querySelectorAll('.structured-array-item').length;
4730
+ const html = template.innerHTML.replace(/__INDEX__/g, String(nextIndex));
4731
+ list.insertAdjacentHTML('beforeend', html);
4732
+ if (typeof initializeTinyMCE === 'function') {
4733
+ initializeTinyMCE();
4734
+ }
4735
+ if (typeof window.initializeQuillEditors === 'function') {
4736
+ window.initializeQuillEditors();
4737
+ }
4738
+ if (typeof initializeMDXEditor === 'function') {
4739
+ initializeMDXEditor();
4740
+ }
4741
+ updateHiddenInput();
4742
+ return;
4743
+ }
4744
+
4745
+ const item = actionButton.closest('.structured-array-item');
4746
+ if (!item || !list) return;
4747
+
4748
+ if (action === 'remove-item') {
4749
+ item.remove();
4750
+ updateHiddenInput();
4751
+ return;
4752
+ }
4753
+
4754
+ if (action === 'move-up') {
4755
+ const previous = item.previousElementSibling;
4756
+ if (previous) {
4757
+ list.insertBefore(item, previous);
4758
+ updateHiddenInput();
4759
+ }
4760
+ return;
4761
+ }
4762
+
4763
+ if (action === 'move-down') {
4764
+ const next = item.nextElementSibling;
4765
+ if (next) {
4766
+ list.insertBefore(next, item);
4767
+ updateHiddenInput();
4768
+ }
4769
+ }
4770
+ });
4771
+
4772
+ container.addEventListener('input', (event) => {
4773
+ const target = event.target;
4774
+ if (!(target instanceof Element)) return;
4775
+ if (target.closest('[data-structured-array-list]')) {
4776
+ updateHiddenInput();
4777
+ }
4778
+ });
4779
+
4780
+ container.addEventListener('change', (event) => {
4781
+ const target = event.target;
4782
+ if (!(target instanceof Element)) return;
4783
+ if (target.closest('[data-structured-array-list]')) {
4784
+ updateHiddenInput();
4785
+ }
4786
+ });
4787
+
4788
+ updateHiddenInput();
4789
+ });
4790
+ }
4791
+
4792
+ window.initializeStructuredFields = initializeStructuredFields;
4793
+
4794
+ if (document.readyState === 'loading') {
4795
+ document.addEventListener('DOMContentLoaded', initializeStructuredFields);
4796
+ } else {
4797
+ initializeStructuredFields();
4798
+ }
4799
+
4800
+ document.addEventListener('htmx:afterSwap', function() {
4801
+ setTimeout(initializeStructuredFields, 50);
4802
+ });
4803
+ } else if (typeof window.initializeStructuredFields === 'function') {
4804
+ window.initializeStructuredFields();
4805
+ }
4806
+ </script>
4807
+ `;
4808
+ }
4809
+ function getBlocksFieldScript() {
4810
+ return `
4811
+ ${getReadFieldValueScript()}
4812
+ <script>
4813
+ if (!window.__sonicBlocksFieldInit) {
4814
+ window.__sonicBlocksFieldInit = true;
4815
+
4816
+ function initializeBlocksFields() {
4817
+ document.querySelectorAll('.blocks-field').forEach((container) => {
4818
+ if (container.dataset.blocksInitialized === 'true') {
4819
+ return;
4820
+ }
4821
+
4822
+ container.dataset.blocksInitialized = 'true';
4823
+ const list = container.querySelector('[data-blocks-list]');
4824
+ const hiddenInput = container.querySelector('input[type="hidden"]');
4825
+ const typeSelect = container.querySelector('[data-role="block-type-select"]');
4826
+ const discriminator = container.dataset.blocksDiscriminator || 'blockType';
4827
+
4828
+ const updateOrderLabels = () => {
4829
+ const items = Array.from(container.querySelectorAll('.blocks-item'));
4830
+ items.forEach((item, index) => {
4831
+ const label = item.querySelector('[data-block-order-label]');
4832
+ if (label) {
4833
+ label.textContent = '#'+ (index + 1);
4834
+ }
4835
+
4836
+ const moveUpButton = item.querySelector('[data-action="move-up"]');
4837
+ if (moveUpButton instanceof HTMLButtonElement) {
4838
+ moveUpButton.disabled = index === 0;
4839
+ }
4840
+
4841
+ const moveDownButton = item.querySelector('[data-action="move-down"]');
4842
+ if (moveDownButton instanceof HTMLButtonElement) {
4843
+ moveDownButton.disabled = index === items.length - 1;
4844
+ }
4845
+ });
4846
+ };
4847
+
4848
+ const readFieldValue = window.sonicReadFieldValue;
4849
+
4850
+ const readBlockItem = (item) => {
4851
+ if (item.dataset.blockRaw) {
4852
+ try {
4853
+ return JSON.parse(item.dataset.blockRaw);
4854
+ } catch (error) {
4855
+ return {};
4856
+ }
4857
+ }
4858
+
4859
+ const blockType = item.dataset.blockType;
4860
+ const data = {};
4861
+
4862
+ item.querySelectorAll('.blocks-subfield').forEach((fieldWrapper) => {
4863
+ const fieldName = fieldWrapper.dataset.blockField;
4864
+ if (!fieldName) {
4865
+ return;
4866
+ }
4867
+ data[fieldName] = readFieldValue(fieldWrapper);
4868
+ });
4869
+
4870
+ return { [discriminator]: blockType, ...data };
4871
+ };
4872
+
4873
+ const updateHiddenInput = () => {
4874
+ if (!hiddenInput || !list) return;
4875
+ const items = Array.from(list.querySelectorAll('.blocks-item, [data-block-raw]'));
4876
+ const blocksData = items.map((item) => readBlockItem(item));
4877
+ hiddenInput.value = JSON.stringify(blocksData);
4878
+
4879
+ const emptyState = list.querySelector('[data-blocks-empty]');
4880
+ if (emptyState) {
4881
+ emptyState.style.display = blocksData.length === 0 ? 'block' : 'none';
4882
+ }
4883
+ updateOrderLabels();
4884
+ };
4885
+
4886
+ const initializeEditors = () => {
4887
+ if (typeof initializeTinyMCE === 'function') {
4888
+ initializeTinyMCE();
4889
+ }
4890
+ if (typeof window.initializeQuillEditors === 'function') {
4891
+ window.initializeQuillEditors();
4892
+ }
4893
+ if (typeof initializeMDXEditor === 'function') {
4894
+ initializeMDXEditor();
4895
+ }
4896
+ };
4897
+
4898
+ if (typeof window.initializeDragSortable === 'function' && list) {
4899
+ window.initializeDragSortable(list, {
4900
+ itemSelector: '.blocks-item',
4901
+ handleSelector: '[data-action="drag-handle"]',
4902
+ onUpdate: updateHiddenInput
4903
+ });
4904
+ }
4905
+
4906
+ container.addEventListener('click', (event) => {
4907
+ const target = event.target;
4908
+ if (!(target instanceof Element)) return;
4909
+ const actionButton = target.closest('[data-action]');
4910
+ if (!actionButton) return;
4911
+
4912
+ if (actionButton.hasAttribute('disabled')) {
4913
+ return;
4914
+ }
4915
+
4916
+ const action = actionButton.getAttribute('data-action');
4917
+ if (action === 'add-block') {
4918
+ const blockType = typeSelect ? typeSelect.value : '';
4919
+ if (!blockType || !list) return;
4920
+ const template = container.querySelector('template[data-block-template="' + blockType + '"]');
4921
+ if (!template) return;
4922
+
4923
+ const nextIndex = list.querySelectorAll('.blocks-item').length;
4924
+ const html = template.innerHTML.replace(/__INDEX__/g, String(nextIndex));
4925
+ list.insertAdjacentHTML('beforeend', html);
4926
+ if (typeSelect) {
4927
+ typeSelect.value = '';
4928
+ }
4929
+ initializeEditors();
4930
+ if (typeof window.initializeStructuredFields === 'function') {
4931
+ window.initializeStructuredFields();
4932
+ }
4933
+ updateHiddenInput();
4934
+ return;
4935
+ }
4936
+
4937
+ const item = actionButton.closest('.blocks-item');
4938
+ if (!item || !list) return;
4939
+
4940
+ if (action === 'remove-block') {
4941
+ item.remove();
4942
+ updateHiddenInput();
4943
+ return;
4944
+ }
4945
+
4946
+ if (action === 'move-up') {
4947
+ const previous = item.previousElementSibling;
4948
+ if (previous) {
4949
+ list.insertBefore(item, previous);
4950
+ updateHiddenInput();
4951
+ }
4952
+ return;
4953
+ }
4954
+
4955
+ if (action === 'move-down') {
4956
+ const next = item.nextElementSibling;
4957
+ if (next) {
4958
+ list.insertBefore(next, item);
4959
+ updateHiddenInput();
4960
+ }
4961
+ }
4962
+ });
4963
+
4964
+ container.addEventListener('input', (event) => {
4965
+ const target = event.target;
4966
+ if (!(target instanceof Element)) return;
4967
+ if (target.closest('[data-blocks-list]')) {
4968
+ updateHiddenInput();
4969
+ }
4970
+ });
4971
+
4972
+ container.addEventListener('change', (event) => {
4973
+ const target = event.target;
4974
+ if (!(target instanceof Element)) return;
4975
+ if (target.closest('[data-blocks-list]')) {
4976
+ updateHiddenInput();
4977
+ }
4978
+ });
4979
+
4980
+ updateHiddenInput();
4981
+ });
4982
+ }
4983
+
4984
+ window.initializeBlocksFields = initializeBlocksFields;
4985
+
4986
+ if (document.readyState === 'loading') {
4987
+ document.addEventListener('DOMContentLoaded', initializeBlocksFields);
4988
+ } else {
4989
+ initializeBlocksFields();
4990
+ }
4991
+
4992
+ document.addEventListener('htmx:afterSwap', function() {
4993
+ setTimeout(initializeBlocksFields, 50);
4994
+ });
4995
+ } else if (typeof window.initializeBlocksFields === 'function') {
4996
+ window.initializeBlocksFields();
4997
+ }
4998
+ </script>
4999
+ `;
5000
+ }
5001
+ function escapeHtml2(text) {
5002
+ if (typeof text !== "string") return String(text || "");
5003
+ return text.replace(/[&<>"']/g, (char) => ({
5004
+ "&": "&amp;",
5005
+ "<": "&lt;",
5006
+ ">": "&gt;",
5007
+ '"': "&quot;",
5008
+ "'": "&#39;"
5009
+ })[char] || char);
5010
+ }
4035
5011
 
4036
5012
  // src/plugins/available/tinymce-plugin/index.ts
4037
- var builder = PluginBuilder.create({
5013
+ var builder = chunkYHW27CBV_cjs.PluginBuilder.create({
4038
5014
  name: "tinymce-plugin",
4039
5015
  version: "1.0.0",
4040
5016
  description: "Powerful WYSIWYG rich text editor for content creation"
@@ -4317,7 +5293,7 @@ function getQuillCDN(version = "2.0.2") {
4317
5293
  `;
4318
5294
  }
4319
5295
  function createQuillEditorPlugin() {
4320
- const builder3 = PluginBuilder.create({
5296
+ const builder3 = chunkYHW27CBV_cjs.PluginBuilder.create({
4321
5297
  name: "quill-editor",
4322
5298
  version: "1.0.0",
4323
5299
  description: "Quill rich text editor integration for SonicJS"
@@ -4343,7 +5319,7 @@ function createQuillEditorPlugin() {
4343
5319
  createQuillEditorPlugin();
4344
5320
 
4345
5321
  // src/plugins/available/easy-mdx/index.ts
4346
- var builder2 = PluginBuilder.create({
5322
+ var builder2 = chunkYHW27CBV_cjs.PluginBuilder.create({
4347
5323
  name: "easy-mdx",
4348
5324
  version: "1.0.0",
4349
5325
  description: "Lightweight markdown editor with live preview"
@@ -4575,17 +5551,24 @@ function renderContentFormPage(data) {
4575
5551
  const coreFieldsHTML = coreFields.sort((a, b) => a.field_order - b.field_order).map((field) => renderDynamicField(field, {
4576
5552
  value: getFieldValue(field.field_name),
4577
5553
  errors: data.validationErrors?.[field.field_name] || [],
4578
- pluginStatuses
5554
+ pluginStatuses,
5555
+ collectionId: data.collection.id,
5556
+ contentId: data.id
5557
+ // Pass content ID when editing
4579
5558
  }));
4580
5559
  const contentFieldsHTML = contentFields.sort((a, b) => a.field_order - b.field_order).map((field) => renderDynamicField(field, {
4581
5560
  value: getFieldValue(field.field_name),
4582
5561
  errors: data.validationErrors?.[field.field_name] || [],
4583
- pluginStatuses
5562
+ pluginStatuses,
5563
+ collectionId: data.collection.id,
5564
+ contentId: data.id
4584
5565
  }));
4585
5566
  const metaFieldsHTML = metaFields.sort((a, b) => a.field_order - b.field_order).map((field) => renderDynamicField(field, {
4586
5567
  value: getFieldValue(field.field_name),
4587
5568
  errors: data.validationErrors?.[field.field_name] || [],
4588
- pluginStatuses
5569
+ pluginStatuses,
5570
+ collectionId: data.collection.id,
5571
+ contentId: data.id
4589
5572
  }));
4590
5573
  const pageContent = `
4591
5574
  <div class="space-y-6">
@@ -4627,8 +5610,8 @@ function renderContentFormPage(data) {
4627
5610
  <!-- Form Content -->
4628
5611
  <div class="px-6 py-6">
4629
5612
  <div id="form-messages">
4630
- ${data.error ? chunkAZLU3ROK_cjs.renderAlert({ type: "error", message: data.error, dismissible: true }) : ""}
4631
- ${data.success ? chunkAZLU3ROK_cjs.renderAlert({ type: "success", message: data.success, dismissible: true }) : ""}
5613
+ ${data.error ? chunkBZC4FYW7_cjs.renderAlert({ type: "error", message: data.error, dismissible: true }) : ""}
5614
+ ${data.success ? chunkBZC4FYW7_cjs.renderAlert({ type: "success", message: data.success, dismissible: true }) : ""}
4632
5615
  </div>
4633
5616
 
4634
5617
  <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
@@ -4863,7 +5846,7 @@ function renderContentFormPage(data) {
4863
5846
  </div>
4864
5847
 
4865
5848
  <!-- Confirmation Dialogs -->
4866
- ${chunkAZLU3ROK_cjs.renderConfirmationDialog({
5849
+ ${chunkBZC4FYW7_cjs.renderConfirmationDialog({
4867
5850
  id: "duplicate-content-confirm",
4868
5851
  title: "Duplicate Content",
4869
5852
  message: "Create a copy of this content?",
@@ -4874,7 +5857,7 @@ function renderContentFormPage(data) {
4874
5857
  onConfirm: "performDuplicateContent()"
4875
5858
  })}
4876
5859
 
4877
- ${chunkAZLU3ROK_cjs.renderConfirmationDialog({
5860
+ ${chunkBZC4FYW7_cjs.renderConfirmationDialog({
4878
5861
  id: "delete-content-confirm",
4879
5862
  title: "Delete Content",
4880
5863
  message: "Are you sure you want to delete this content? This action cannot be undone.",
@@ -4885,7 +5868,7 @@ function renderContentFormPage(data) {
4885
5868
  onConfirm: `performDeleteContent('${data.id}')`
4886
5869
  })}
4887
5870
 
4888
- ${chunkAZLU3ROK_cjs.getConfirmationDialogScript()}
5871
+ ${chunkBZC4FYW7_cjs.getConfirmationDialogScript()}
4889
5872
 
4890
5873
  ${data.tinymceEnabled ? getTinyMCEScript(data.tinymceSettings?.apiKey) : "<!-- TinyMCE plugin not active -->"}
4891
5874
 
@@ -4971,71 +5954,371 @@ function renderContentFormPage(data) {
4971
5954
  }
4972
5955
  }
4973
5956
 
4974
- // Close modal
4975
- closeMediaSelector();
4976
- }
5957
+ // Close modal
5958
+ closeMediaSelector();
5959
+ }
5960
+
5961
+ function clearMediaField(fieldId) {
5962
+ const hiddenInput = document.getElementById(fieldId);
5963
+ const preview = document.getElementById(fieldId + '-preview');
5964
+
5965
+ if (hiddenInput) {
5966
+ hiddenInput.value = '';
5967
+ }
5968
+
5969
+ if (preview) {
5970
+ // Clear all children if it's a grid, or hide it
5971
+ if (preview.classList.contains('media-preview-grid')) {
5972
+ preview.innerHTML = '';
5973
+ }
5974
+ preview.classList.add('hidden');
5975
+ }
5976
+ }
5977
+
5978
+ // Global function to remove a single media from multiple selection
5979
+ window.removeMediaFromMultiple = function(fieldId, urlToRemove) {
5980
+ const hiddenInput = document.getElementById(fieldId);
5981
+ if (!hiddenInput) return;
5982
+
5983
+ const values = hiddenInput.value.split(',').filter(url => url !== urlToRemove);
5984
+ hiddenInput.value = values.join(',');
5985
+
5986
+ // Remove preview item
5987
+ const previewItem = document.querySelector(\`[data-url="\${urlToRemove}"]\`);
5988
+ if (previewItem) {
5989
+ previewItem.remove();
5990
+ }
5991
+
5992
+ // Hide preview grid if empty
5993
+ if (values.length === 0) {
5994
+ const preview = document.getElementById(fieldId + '-preview');
5995
+ if (preview) {
5996
+ preview.classList.add('hidden');
5997
+ }
5998
+ }
5999
+ };
6000
+
6001
+ // Global function called by media selector buttons
6002
+ window.selectMediaFile = function(mediaId, mediaUrl, filename) {
6003
+ if (!currentMediaFieldId) {
6004
+ console.error('No field ID set for media selection');
6005
+ return;
6006
+ }
6007
+
6008
+ const fieldId = currentMediaFieldId;
6009
+
6010
+ // Set the hidden input value to the media URL (not ID)
6011
+ const hiddenInput = document.getElementById(fieldId);
6012
+ if (hiddenInput) {
6013
+ hiddenInput.value = mediaUrl;
6014
+ }
6015
+
6016
+ // Update the preview
6017
+ const preview = document.getElementById(fieldId + '-preview');
6018
+ if (preview) {
6019
+ preview.innerHTML = \`<img src="\${mediaUrl}" alt="\${filename}" class="w-32 h-32 object-cover rounded-lg border border-white/20">\`;
6020
+ preview.classList.remove('hidden');
6021
+ }
6022
+
6023
+ // Show the remove button by finding the media actions container and updating it
6024
+ const mediaField = hiddenInput?.closest('.media-field-container');
6025
+ if (mediaField) {
6026
+ const actionsDiv = mediaField.querySelector('.media-actions');
6027
+ if (actionsDiv && !actionsDiv.querySelector('button:has-text("Remove")')) {
6028
+ const removeBtn = document.createElement('button');
6029
+ removeBtn.type = 'button';
6030
+ removeBtn.onclick = () => clearMediaField(fieldId);
6031
+ removeBtn.className = 'inline-flex items-center px-4 py-2 bg-red-600 text-white rounded-xl hover:bg-red-700 transition-all';
6032
+ removeBtn.textContent = 'Remove';
6033
+ actionsDiv.appendChild(removeBtn);
6034
+ }
6035
+ }
6036
+
6037
+ // DON'T close the modal - let user click OK button
6038
+ // Visual feedback: highlight the selected item
6039
+ document.querySelectorAll('#media-selector-grid [data-media-id]').forEach(el => {
6040
+ el.classList.remove('ring-2', 'ring-lime-500', 'dark:ring-lime-400');
6041
+ });
6042
+ const selectedItem = document.querySelector(\`#media-selector-grid [data-media-id="\${mediaId}"]\`);
6043
+ if (selectedItem) {
6044
+ selectedItem.classList.add('ring-2', 'ring-lime-500', 'dark:ring-lime-400');
6045
+ }
6046
+ };
6047
+
6048
+ function setMediaField(fieldId, mediaUrl) {
6049
+ document.getElementById(fieldId).value = mediaUrl;
6050
+ const preview = document.getElementById(fieldId + '-preview');
6051
+ preview.innerHTML = \`<img src="\${mediaUrl}" alt="Selected media" class="w-32 h-32 object-cover rounded-lg ring-1 ring-zinc-950/10 dark:ring-white/10">\`;
6052
+ preview.classList.remove('hidden');
6053
+
6054
+ // Close modal
6055
+ document.querySelector('.fixed.inset-0')?.remove();
6056
+ }
6057
+
6058
+ // Reference field functions
6059
+ let currentReferenceFieldId = null;
6060
+ let referenceSearchTimeout = null;
6061
+
6062
+ function getReferenceContainer(fieldId) {
6063
+ const input = document.getElementById(fieldId);
6064
+ return input ? input.closest('[data-reference-field]') : null;
6065
+ }
6066
+
6067
+ function getReferenceCollections(container) {
6068
+ if (!container) return [];
6069
+ const rawCollections = container.dataset.referenceCollections || '';
6070
+ const collections = rawCollections
6071
+ .split(',')
6072
+ .map((value) => value.trim())
6073
+ .filter(Boolean);
6074
+ if (collections.length > 0) {
6075
+ return collections;
6076
+ }
6077
+ const singleCollection = container.dataset.referenceCollection;
6078
+ return singleCollection ? [singleCollection] : [];
6079
+ }
6080
+
6081
+ async function fetchReferenceItems(collections, search = '', limit = 20) {
6082
+ const params = new URLSearchParams({ limit: String(limit) });
6083
+ collections.forEach((collection) => params.append('collection', collection));
6084
+ if (search) {
6085
+ params.set('search', search);
6086
+ }
6087
+ const response = await fetch('/admin/api/references?' + params.toString());
6088
+ if (!response.ok) {
6089
+ throw new Error('Failed to load references');
6090
+ }
6091
+ const data = await response.json();
6092
+ return data?.data || [];
6093
+ }
6094
+
6095
+ async function fetchReferenceById(collections, id) {
6096
+ if (!id) return null;
6097
+ const params = new URLSearchParams({ id });
6098
+ collections.forEach((collection) => params.append('collection', collection));
6099
+ const response = await fetch('/admin/api/references?' + params.toString());
6100
+ if (!response.ok) {
6101
+ return null;
6102
+ }
6103
+ const data = await response.json();
6104
+ return data?.data || null;
6105
+ }
6106
+
6107
+ function renderReferenceDisplay(container, item, fallbackMessage = 'No reference selected.') {
6108
+ const display = container.querySelector('[data-reference-display]');
6109
+ const removeButton = container.querySelector('[data-reference-clear]');
6110
+ if (!display) return;
6111
+
6112
+ display.innerHTML = '';
6113
+
6114
+ if (!item) {
6115
+ display.textContent = fallbackMessage;
6116
+ if (removeButton) {
6117
+ removeButton.disabled = true;
6118
+ }
6119
+ return;
6120
+ }
6121
+
6122
+ const title = item.title || item.slug || item.id || 'Untitled';
6123
+ const titleEl = document.createElement('div');
6124
+ titleEl.className = 'font-medium text-zinc-900 dark:text-white';
6125
+ titleEl.textContent = title;
6126
+
6127
+ display.appendChild(titleEl);
6128
+
6129
+ const metaRow = document.createElement('div');
6130
+ metaRow.className = 'mt-1 flex flex-wrap items-center gap-2 text-xs text-zinc-500 dark:text-zinc-400';
6131
+
6132
+ if (item.collection?.display_name || item.collection?.name) {
6133
+ const collectionLabel = document.createElement('span');
6134
+ collectionLabel.className = 'inline-flex items-center rounded-full bg-zinc-100 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-zinc-600 dark:bg-white/10 dark:text-zinc-200';
6135
+ collectionLabel.textContent = item.collection.display_name || item.collection.name;
6136
+ metaRow.appendChild(collectionLabel);
6137
+ }
6138
+
6139
+ if (item.slug) {
6140
+ const slugEl = document.createElement('span');
6141
+ slugEl.textContent = item.slug;
6142
+ metaRow.appendChild(slugEl);
6143
+ }
6144
+
6145
+ if (metaRow.childElementCount > 0) {
6146
+ display.appendChild(metaRow);
6147
+ }
6148
+
6149
+ if (removeButton) {
6150
+ removeButton.disabled = false;
6151
+ }
6152
+ }
6153
+
6154
+ function updateReferenceField(fieldId, item) {
6155
+ const input = document.getElementById(fieldId);
6156
+ const container = getReferenceContainer(fieldId);
6157
+ if (!input || !container) return;
6158
+
6159
+ input.value = item?.id || '';
6160
+ renderReferenceDisplay(container, item, 'No reference selected.');
6161
+ input.dispatchEvent(new Event('input', { bubbles: true }));
6162
+ input.dispatchEvent(new Event('change', { bubbles: true }));
6163
+ }
6164
+
6165
+ function clearReferenceField(fieldId) {
6166
+ updateReferenceField(fieldId, null);
6167
+ }
6168
+
6169
+ function closeReferenceSelector() {
6170
+ const modal = document.getElementById('reference-selector-modal');
6171
+ if (modal) {
6172
+ modal.remove();
6173
+ }
6174
+ currentReferenceFieldId = null;
6175
+ }
6176
+
6177
+ function openReferenceSelector(fieldId) {
6178
+ const container = getReferenceContainer(fieldId);
6179
+ const collections = getReferenceCollections(container);
6180
+ if (!container || collections.length === 0) {
6181
+ console.error('Reference collection is missing for field', fieldId);
6182
+ return;
6183
+ }
6184
+
6185
+ currentReferenceFieldId = fieldId;
6186
+
6187
+ const modal = document.createElement('div');
6188
+ modal.className = 'fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50';
6189
+ modal.id = 'reference-selector-modal';
6190
+ modal.innerHTML = \`
6191
+ <div class="rounded-xl bg-white dark:bg-zinc-900 shadow-xl ring-1 ring-zinc-950/5 dark:ring-white/10 p-6 w-full max-w-3xl max-h-[90vh] overflow-y-auto">
6192
+ <div class="flex items-center justify-between gap-3">
6193
+ <h3 class="text-lg font-semibold text-zinc-950 dark:text-white">Select Reference</h3>
6194
+ <button
6195
+ type="button"
6196
+ onclick="closeReferenceSelector()"
6197
+ class="rounded-md text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-300"
6198
+ aria-label="Close"
6199
+ >
6200
+ \u2715
6201
+ </button>
6202
+ </div>
6203
+ <div class="mt-4">
6204
+ <input
6205
+ type="search"
6206
+ id="reference-search-input"
6207
+ placeholder="Search by title or slug..."
6208
+ class="w-full rounded-lg border border-zinc-200 bg-white px-3 py-2 text-sm text-zinc-900 shadow-sm focus:border-cyan-500 focus:ring-cyan-500 dark:border-white/10 dark:bg-zinc-900 dark:text-white"
6209
+ >
6210
+ </div>
6211
+ <div id="reference-results" class="mt-4 space-y-2"></div>
6212
+ <div class="mt-4 flex justify-end">
6213
+ <button
6214
+ type="button"
6215
+ onclick="closeReferenceSelector()"
6216
+ class="rounded-lg bg-zinc-950 dark:bg-white px-4 py-2 text-sm font-semibold text-white dark:text-zinc-950 hover:bg-zinc-800 dark:hover:bg-zinc-100 transition-colors"
6217
+ >
6218
+ Close
6219
+ </button>
6220
+ </div>
6221
+ </div>
6222
+ \`;
6223
+
6224
+ document.body.appendChild(modal);
6225
+
6226
+ const resultsContainer = modal.querySelector('#reference-results');
6227
+ const searchInput = modal.querySelector('#reference-search-input');
6228
+
6229
+ const renderResults = (items) => {
6230
+ resultsContainer.innerHTML = '';
6231
+ if (!items || items.length === 0) {
6232
+ resultsContainer.innerHTML = '<div class="rounded-lg border border-dashed border-zinc-200 p-4 text-sm text-zinc-500 dark:border-white/10 dark:text-zinc-400">No items found.</div>';
6233
+ return;
6234
+ }
6235
+
6236
+ const selectedId = document.getElementById(fieldId)?.value;
6237
+
6238
+ items.forEach((item) => {
6239
+ const button = document.createElement('button');
6240
+ button.type = 'button';
6241
+ button.className = 'w-full text-left rounded-lg border border-zinc-200 px-4 py-3 text-sm text-zinc-700 hover:bg-zinc-50 dark:border-white/10 dark:text-zinc-200 dark:hover:bg-white/5';
6242
+ if (item.id === selectedId) {
6243
+ button.classList.add('ring-2', 'ring-cyan-500', 'dark:ring-cyan-400');
6244
+ }
6245
+
6246
+ const title = item.title || item.slug || item.id || 'Untitled';
6247
+ const titleEl = document.createElement('div');
6248
+ titleEl.className = 'font-medium text-zinc-900 dark:text-white';
6249
+ titleEl.textContent = title;
4977
6250
 
4978
- function clearMediaField(fieldId) {
4979
- document.getElementById(fieldId).value = '';
4980
- document.getElementById(fieldId + '-preview').classList.add('hidden');
4981
- }
6251
+ button.appendChild(titleEl);
4982
6252
 
4983
- // Global function called by media selector buttons
4984
- window.selectMediaFile = function(mediaId, mediaUrl, filename) {
4985
- if (!currentMediaFieldId) {
4986
- console.error('No field ID set for media selection');
4987
- return;
4988
- }
6253
+ const metaRow = document.createElement('div');
6254
+ metaRow.className = 'mt-1 flex flex-wrap items-center gap-2 text-xs text-zinc-500 dark:text-zinc-400';
4989
6255
 
4990
- const fieldId = currentMediaFieldId;
6256
+ if (item.collection?.display_name || item.collection?.name) {
6257
+ const collectionLabel = document.createElement('span');
6258
+ collectionLabel.className = 'inline-flex items-center rounded-full bg-zinc-100 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-zinc-600 dark:bg-white/10 dark:text-zinc-200';
6259
+ collectionLabel.textContent = item.collection.display_name || item.collection.name;
6260
+ metaRow.appendChild(collectionLabel);
6261
+ }
4991
6262
 
4992
- // Set the hidden input value to the media URL (not ID)
4993
- const hiddenInput = document.getElementById(fieldId);
4994
- if (hiddenInput) {
4995
- hiddenInput.value = mediaUrl;
4996
- }
6263
+ if (item.slug) {
6264
+ const slugEl = document.createElement('span');
6265
+ slugEl.textContent = item.slug;
6266
+ metaRow.appendChild(slugEl);
6267
+ }
4997
6268
 
4998
- // Update the preview
4999
- const preview = document.getElementById(fieldId + '-preview');
5000
- if (preview) {
5001
- preview.innerHTML = \`<img src="\${mediaUrl}" alt="\${filename}" class="w-32 h-32 object-cover rounded-lg border border-white/20">\`;
5002
- preview.classList.remove('hidden');
5003
- }
6269
+ if (metaRow.childElementCount > 0) {
6270
+ button.appendChild(metaRow);
6271
+ }
5004
6272
 
5005
- // Show the remove button by finding the media actions container and updating it
5006
- const mediaField = hiddenInput?.closest('.media-field-container');
5007
- if (mediaField) {
5008
- const actionsDiv = mediaField.querySelector('.media-actions');
5009
- if (actionsDiv && !actionsDiv.querySelector('button:has-text("Remove")')) {
5010
- const removeBtn = document.createElement('button');
5011
- removeBtn.type = 'button';
5012
- removeBtn.onclick = () => clearMediaField(fieldId);
5013
- removeBtn.className = 'inline-flex items-center px-4 py-2 bg-red-600 text-white rounded-xl hover:bg-red-700 transition-all';
5014
- removeBtn.textContent = 'Remove';
5015
- actionsDiv.appendChild(removeBtn);
6273
+ button.addEventListener('click', () => {
6274
+ updateReferenceField(fieldId, item);
6275
+ closeReferenceSelector();
6276
+ });
6277
+
6278
+ resultsContainer.appendChild(button);
6279
+ });
6280
+ };
6281
+
6282
+ const loadResults = async (searchValue = '') => {
6283
+ try {
6284
+ const items = await fetchReferenceItems(collections, searchValue);
6285
+ renderResults(items);
6286
+ } catch (error) {
6287
+ resultsContainer.innerHTML = '<div class="rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-700 dark:border-amber-500/30 dark:bg-amber-500/10 dark:text-amber-200">Failed to load references.</div>';
5016
6288
  }
5017
- }
6289
+ };
5018
6290
 
5019
- // DON'T close the modal - let user click OK button
5020
- // Visual feedback: highlight the selected item
5021
- document.querySelectorAll('#media-selector-grid [data-media-id]').forEach(el => {
5022
- el.classList.remove('ring-2', 'ring-lime-500', 'dark:ring-lime-400');
6291
+ loadResults();
6292
+
6293
+ searchInput.addEventListener('input', () => {
6294
+ if (referenceSearchTimeout) {
6295
+ clearTimeout(referenceSearchTimeout);
6296
+ }
6297
+ referenceSearchTimeout = setTimeout(() => {
6298
+ loadResults(searchInput.value.trim());
6299
+ }, 250);
5023
6300
  });
5024
- const selectedItem = document.querySelector(\`#media-selector-grid [data-media-id="\${mediaId}"]\`);
5025
- if (selectedItem) {
5026
- selectedItem.classList.add('ring-2', 'ring-lime-500', 'dark:ring-lime-400');
5027
- }
5028
- };
6301
+ }
5029
6302
 
5030
- function setMediaField(fieldId, mediaUrl) {
5031
- document.getElementById(fieldId).value = mediaUrl;
5032
- const preview = document.getElementById(fieldId + '-preview');
5033
- preview.innerHTML = \`<img src="\${mediaUrl}" alt="Selected media" class="w-32 h-32 object-cover rounded-lg ring-1 ring-zinc-950/10 dark:ring-white/10">\`;
5034
- preview.classList.remove('hidden');
6303
+ document.addEventListener('DOMContentLoaded', () => {
6304
+ document.querySelectorAll('[data-reference-field]').forEach(async (container) => {
6305
+ const input = container.querySelector('input[type="hidden"]');
6306
+ const collections = getReferenceCollections(container);
6307
+ if (!input || collections.length === 0) return;
5035
6308
 
5036
- // Close modal
5037
- document.querySelector('.fixed.inset-0')?.remove();
5038
- }
6309
+ if (!input.value) {
6310
+ renderReferenceDisplay(container, null, 'No reference selected.');
6311
+ return;
6312
+ }
6313
+
6314
+ const item = await fetchReferenceById(collections, input.value);
6315
+ if (item) {
6316
+ renderReferenceDisplay(container, item);
6317
+ } else {
6318
+ renderReferenceDisplay(container, null, 'Reference not found.');
6319
+ }
6320
+ });
6321
+ });
5039
6322
 
5040
6323
  // Custom select options
5041
6324
  function addCustomOption(input, selectId) {
@@ -5190,11 +6473,11 @@ function renderContentFormPage(data) {
5190
6473
  content: pageContent,
5191
6474
  version: data.version
5192
6475
  };
5193
- return chunkAZLU3ROK_cjs.renderAdminLayoutCatalyst(layoutData);
6476
+ return chunkBZC4FYW7_cjs.renderAdminLayoutCatalyst(layoutData);
5194
6477
  }
5195
6478
 
5196
6479
  // src/templates/pages/admin-content-list.template.ts
5197
- chunkAZLU3ROK_cjs.init_admin_layout_catalyst_template();
6480
+ chunkBZC4FYW7_cjs.init_admin_layout_catalyst_template();
5198
6481
  function renderContentListPage(data) {
5199
6482
  const urlParams = new URLSearchParams();
5200
6483
  if (data.modelName && data.modelName !== "all") urlParams.set("model", data.modelName);
@@ -5202,7 +6485,7 @@ function renderContentListPage(data) {
5202
6485
  if (data.search) urlParams.set("search", data.search);
5203
6486
  if (data.page && data.page !== 1) urlParams.set("page", data.page.toString());
5204
6487
  const currentParams = urlParams.toString();
5205
- const hasActiveFilters = data.modelName !== "all" || data.status !== "all" || !!data.search;
6488
+ data.modelName !== "all" || data.status !== "all" || !!data.search;
5206
6489
  const filterBarData = {
5207
6490
  filters: [
5208
6491
  {
@@ -5232,6 +6515,11 @@ function renderContentListPage(data) {
5232
6515
  }
5233
6516
  ],
5234
6517
  actions: [
6518
+ {
6519
+ label: "Advanced Search",
6520
+ className: "btn-primary",
6521
+ onclick: "openAdvancedSearch()"
6522
+ },
5235
6523
  {
5236
6524
  label: "Refresh",
5237
6525
  className: "btn-secondary",
@@ -5383,12 +6671,57 @@ function renderContentListPage(data) {
5383
6671
  <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 rounded-xl">
5384
6672
  <div class="px-6 py-5">
5385
6673
  <div class="flex items-center justify-between">
5386
- <div class="flex items-center space-x-4">
5387
- <!-- Search Input -->
6674
+ <div class="flex items-center space-x-4 flex-1">
6675
+ <!-- Model Filter -->
6676
+ <div>
6677
+ <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Model</label>
6678
+ <div class="grid grid-cols-1">
6679
+ <select
6680
+ name="model"
6681
+ onchange="updateContentFilters('model', this.value)"
6682
+ class="col-start-1 row-start-1 w-full appearance-none rounded-lg bg-white/5 dark:bg-white/5 py-2 pl-3 pr-8 text-sm 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 min-w-40"
6683
+ >
6684
+ <option value="all" ${data.modelName === "all" ? "selected" : ""}>All Models</option>
6685
+ ${data.models.map((model) => `
6686
+ <option value="${model.name}" ${data.modelName === model.name ? "selected" : ""}>
6687
+ ${model.displayName}
6688
+ </option>
6689
+ `).join("")}
6690
+ </select>
6691
+ <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">
6692
+ <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" />
6693
+ </svg>
6694
+ </div>
6695
+ </div>
6696
+
6697
+ <!-- Status Filter -->
5388
6698
  <div>
6699
+ <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Status</label>
6700
+ <div class="grid grid-cols-1">
6701
+ <select
6702
+ name="status"
6703
+ onchange="updateContentFilters('status', this.value)"
6704
+ class="col-start-1 row-start-1 w-full appearance-none rounded-lg bg-white/5 dark:bg-white/5 py-2 pl-3 pr-8 text-sm 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 min-w-40"
6705
+ >
6706
+ <option value="all" ${data.status === "all" ? "selected" : ""}>All Status</option>
6707
+ <option value="draft" ${data.status === "draft" ? "selected" : ""}>Draft</option>
6708
+ <option value="review" ${data.status === "review" ? "selected" : ""}>Under Review</option>
6709
+ <option value="scheduled" ${data.status === "scheduled" ? "selected" : ""}>Scheduled</option>
6710
+ <option value="published" ${data.status === "published" ? "selected" : ""}>Published</option>
6711
+ <option value="archived" ${data.status === "archived" ? "selected" : ""}>Archived</option>
6712
+ <option value="deleted" ${data.status === "deleted" ? "selected" : ""}>Deleted</option>
6713
+ </select>
6714
+ <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">
6715
+ <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" />
6716
+ </svg>
6717
+ </div>
6718
+ </div>
6719
+
6720
+ <!-- Search Input -->
6721
+ <div class="flex-1 max-w-md">
5389
6722
  <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Search</label>
5390
6723
  <form onsubmit="performContentSearch(event)" class="flex items-center space-x-2">
5391
- <div class="relative group">
6724
+ <div class="relative group flex-1">
5392
6725
  <input
5393
6726
  type="text"
5394
6727
  name="search"
@@ -5396,7 +6729,7 @@ function renderContentListPage(data) {
5396
6729
  value="${data.search || ""}"
5397
6730
  oninput="toggleContentClearButton()"
5398
6731
  placeholder="Search content..."
5399
- class="rounded-full bg-white/90 dark:bg-zinc-800/90 backdrop-blur-sm px-4 py-2.5 pl-11 pr-10 text-sm w-72 text-zinc-950 dark:text-white border-2 border-cyan-200/50 dark:border-cyan-700/50 placeholder:text-zinc-400 dark:placeholder:text-zinc-500 focus:outline-none focus:border-cyan-500 dark:focus:border-cyan-400 focus:bg-white dark:focus:bg-zinc-800 focus:shadow-lg focus:shadow-cyan-500/20 dark:focus:shadow-cyan-400/20 transition-all duration-300"
6732
+ class="w-full rounded-full bg-white/90 dark:bg-zinc-800/90 backdrop-blur-sm px-4 py-2.5 pl-11 pr-10 text-sm text-zinc-950 dark:text-white border-2 border-cyan-200/50 dark:border-cyan-700/50 placeholder:text-zinc-400 dark:placeholder:text-zinc-500 focus:outline-none focus:border-cyan-500 dark:focus:border-cyan-400 focus:bg-white dark:focus:bg-zinc-800 focus:shadow-lg focus:shadow-cyan-500/20 dark:focus:shadow-cyan-400/20 transition-all duration-300"
5400
6733
  >
5401
6734
  <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">
5402
6735
  <svg class="h-3 w-3 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
@@ -5468,57 +6801,6 @@ function renderContentListPage(data) {
5468
6801
  }
5469
6802
  </script>
5470
6803
  </div>
5471
-
5472
- ${filterBarData.filters.map((filter) => {
5473
- const selectedOption = filter.options.find((opt) => opt.selected);
5474
- const selectedColor = selectedOption?.color || "cyan";
5475
- const colorMap = {
5476
- "cyan": "bg-cyan-400 dark:bg-cyan-400",
5477
- "lime": "bg-lime-400 dark:bg-lime-400",
5478
- "pink": "bg-pink-400 dark:bg-pink-400",
5479
- "purple": "bg-purple-400 dark:bg-purple-400",
5480
- "amber": "bg-amber-400 dark:bg-amber-400",
5481
- "zinc": "bg-zinc-400 dark:bg-zinc-400"
5482
- };
5483
- return `
5484
- <div>
5485
- <label class="block text-sm/6 font-medium text-zinc-950 dark:text-white">${filter.label}</label>
5486
- <div class="mt-2 grid grid-cols-1">
5487
- <div class="col-start-1 row-start-1 flex items-center gap-3 pl-3 pr-8 pointer-events-none">
5488
- ${filter.name === "status" ? `<span class="inline-block size-2 shrink-0 rounded-full border border-transparent ${colorMap[selectedColor]}"></span>` : ""}
5489
- </div>
5490
- <select
5491
- name="${filter.name}"
5492
- onchange="updateContentFilters('${filter.name}', this.value)"
5493
- class="col-start-1 row-start-1 w-full appearance-none rounded-md bg-white/5 dark:bg-white/5 py-1.5 ${filter.name === "status" ? "pl-8" : "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"
5494
- >
5495
- ${filter.options.map((opt) => `
5496
- <option value="${opt.value}" ${opt.selected ? "selected" : ""}>${opt.label}</option>
5497
- `).join("")}
5498
- </select>
5499
- <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">
5500
- <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" />
5501
- </svg>
5502
- </div>
5503
- </div>
5504
- `;
5505
- }).join("")}
5506
-
5507
- <!-- Clear Filters Button -->
5508
- ${hasActiveFilters ? `
5509
- <div>
5510
- <label class="block text-sm/6 font-medium text-zinc-950 dark:text-white mb-2">&nbsp;</label>
5511
- <button
5512
- onclick="clearAllFilters()"
5513
- class="inline-flex items-center gap-x-1.5 px-3 py-2 bg-pink-50 dark:bg-pink-500/10 text-pink-700 dark:text-pink-300 text-sm font-medium rounded-md ring-1 ring-inset ring-pink-600/20 dark:ring-pink-500/20 hover:bg-pink-100 dark:hover:bg-pink-500/20 transition-colors"
5514
- >
5515
- <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
5516
- <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/>
5517
- </svg>
5518
- Clear Filters
5519
- </button>
5520
- </div>
5521
- ` : ""}
5522
6804
  </div>
5523
6805
  <div class="flex items-center gap-x-3">
5524
6806
  <span class="text-sm/6 font-medium text-zinc-700 dark:text-zinc-300 px-3 py-1.5 rounded-full bg-white/60 dark:bg-zinc-800/60 backdrop-blur-sm">${data.totalItems} ${data.totalItems === 1 ? "item" : "items"}</span>
@@ -5599,8 +6881,8 @@ function renderContentListPage(data) {
5599
6881
 
5600
6882
  <!-- Content List -->
5601
6883
  <div id="content-list">
5602
- ${chunkAZLU3ROK_cjs.renderTable(tableData)}
5603
- ${chunkAZLU3ROK_cjs.renderPagination(paginationData)}
6884
+ ${chunkBZC4FYW7_cjs.renderTable(tableData)}
6885
+ ${chunkBZC4FYW7_cjs.renderPagination(paginationData)}
5604
6886
  </div>
5605
6887
 
5606
6888
  </div>
@@ -5758,70 +7040,356 @@ function renderContentListPage(data) {
5758
7040
  function executeBulkAction() {
5759
7041
  if (!currentBulkAction || currentSelectedIds.length === 0) return;
5760
7042
 
5761
- // Close dropdown
5762
- const menu = document.getElementById('bulk-actions-menu');
5763
- menu.classList.add('hidden');
7043
+ // Close dropdown
7044
+ const menu = document.getElementById('bulk-actions-menu');
7045
+ menu.classList.add('hidden');
7046
+
7047
+ fetch('/admin/content/bulk-action', {
7048
+ method: 'POST',
7049
+ headers: {
7050
+ 'Content-Type': 'application/json'
7051
+ },
7052
+ body: JSON.stringify({
7053
+ action: currentBulkAction,
7054
+ ids: currentSelectedIds
7055
+ })
7056
+ })
7057
+ .then(res => res.json())
7058
+ .then(data => {
7059
+ if (data.success) {
7060
+ location.reload();
7061
+ } else {
7062
+ alert('Error: ' + (data.error || 'Unknown error'));
7063
+ }
7064
+ })
7065
+ .catch(err => {
7066
+ console.error('Bulk action error:', err);
7067
+ alert('Failed to perform bulk action');
7068
+ })
7069
+ .finally(() => {
7070
+ // Clear context
7071
+ currentBulkAction = null;
7072
+ currentSelectedIds = [];
7073
+ });
7074
+ }
7075
+
7076
+ // Helper to get action text for display
7077
+ function getActionText(action) {
7078
+ const actionCount = currentSelectedIds.length;
7079
+ switch(action) {
7080
+ case 'publish':
7081
+ return \`publish \${actionCount} item\${actionCount > 1 ? 's' : ''}\`;
7082
+ case 'draft':
7083
+ return \`move \${actionCount} item\${actionCount > 1 ? 's' : ''} to draft\`;
7084
+ case 'delete':
7085
+ return \`delete \${actionCount} item\${actionCount > 1 ? 's' : ''}\`;
7086
+ default:
7087
+ return \`perform action on \${actionCount} item\${actionCount > 1 ? 's' : ''}\`;
7088
+ }
7089
+ }
7090
+
7091
+ </script>
7092
+
7093
+ <!-- Confirmation Dialog for Bulk Actions -->
7094
+ ${chunkBZC4FYW7_cjs.renderConfirmationDialog({
7095
+ id: "bulk-action-confirm",
7096
+ title: "Confirm Bulk Action",
7097
+ message: "Are you sure you want to perform this action? This operation will affect multiple items.",
7098
+ confirmText: "Confirm",
7099
+ cancelText: "Cancel",
7100
+ confirmClass: "bg-blue-500 hover:bg-blue-400",
7101
+ iconColor: "blue",
7102
+ onConfirm: "executeBulkAction()"
7103
+ })}
7104
+
7105
+ <!-- Confirmation Dialog Script -->
7106
+ ${chunkBZC4FYW7_cjs.getConfirmationDialogScript()}
7107
+
7108
+ <!-- Advanced Search Modal -->
7109
+ <div id="advancedSearchModal" class="hidden fixed inset-0 z-50 overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true">
7110
+ <div class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
7111
+ <!-- Background overlay -->
7112
+ <div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" onclick="closeAdvancedSearch()"></div>
7113
+
7114
+ <!-- Modal panel -->
7115
+ <div class="inline-block align-bottom bg-white dark:bg-zinc-900 rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-4xl sm:w-full">
7116
+ <div class="bg-white dark:bg-zinc-900 px-4 pt-5 pb-4 sm:p-6">
7117
+ <!-- Header -->
7118
+ <div class="flex items-center justify-between mb-4">
7119
+ <h3 class="text-lg font-semibold text-zinc-950 dark:text-white" id="modal-title">
7120
+ \u{1F50D} Advanced Search
7121
+ </h3>
7122
+ <button onclick="closeAdvancedSearch()" class="text-zinc-400 hover:text-zinc-500 dark:hover:text-zinc-300">
7123
+ <svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
7124
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
7125
+ </svg>
7126
+ </button>
7127
+ </div>
7128
+
7129
+ <!-- Search Form -->
7130
+ <form id="advancedSearchForm" class="space-y-4">
7131
+ <!-- Search Input -->
7132
+ <div>
7133
+ <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Search Query</label>
7134
+ <div class="relative">
7135
+ <input
7136
+ type="text"
7137
+ id="searchQuery"
7138
+ name="query"
7139
+ placeholder="Enter your search query..."
7140
+ class="w-full rounded-lg bg-white dark:bg-white/5 px-4 py-3 text-sm text-zinc-950 dark:text-white ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 focus:ring-2 focus:ring-indigo-500"
7141
+ autocomplete="off"
7142
+ />
7143
+ <div id="searchSuggestions" class="hidden absolute z-10 w-full mt-1 bg-white dark:bg-zinc-800 rounded-lg shadow-lg border border-zinc-200 dark:border-zinc-700 max-h-60 overflow-y-auto"></div>
7144
+ </div>
7145
+ </div>
7146
+
7147
+ <!-- Mode Toggle -->
7148
+ <div>
7149
+ <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Search Mode</label>
7150
+ <div class="flex gap-4">
7151
+ <label class="flex items-center">
7152
+ <input type="radio" name="mode" value="ai" checked class="mr-2">
7153
+ <span class="text-sm text-zinc-950 dark:text-white">\u{1F916} AI Search (Semantic)</span>
7154
+ </label>
7155
+ <label class="flex items-center">
7156
+ <input type="radio" name="mode" value="keyword" class="mr-2">
7157
+ <span class="text-sm text-zinc-950 dark:text-white">\u{1F524} Keyword Search</span>
7158
+ </label>
7159
+ </div>
7160
+ </div>
7161
+
7162
+ <!-- Filters -->
7163
+ <div class="border-t border-zinc-200 dark:border-zinc-800 pt-4">
7164
+ <h4 class="text-sm font-semibold text-zinc-950 dark:text-white mb-3">Filters</h4>
7165
+
7166
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
7167
+ <!-- Collection Filter -->
7168
+ <div>
7169
+ <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Collections</label>
7170
+ <select
7171
+ id="filterCollections"
7172
+ name="collections"
7173
+ multiple
7174
+ class="w-full rounded-lg bg-white dark:bg-white/5 px-3 py-2 text-sm text-zinc-950 dark:text-white ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10"
7175
+ size="4"
7176
+ >
7177
+ <option value="">All Collections</option>
7178
+ ${data.models.map(
7179
+ (model) => `
7180
+ <option value="${model.name}">${model.displayName}</option>
7181
+ `
7182
+ ).join("")}
7183
+ </select>
7184
+ <p class="text-xs text-zinc-500 dark:text-zinc-400 mt-1">Hold Ctrl/Cmd to select multiple</p>
7185
+ </div>
7186
+
7187
+ <!-- Status Filter -->
7188
+ <div>
7189
+ <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Status</label>
7190
+ <select
7191
+ id="filterStatus"
7192
+ name="status"
7193
+ multiple
7194
+ class="w-full rounded-lg bg-white dark:bg-white/5 px-3 py-2 text-sm text-zinc-950 dark:text-white ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10"
7195
+ size="4"
7196
+ >
7197
+ <option value="published">Published</option>
7198
+ <option value="draft">Draft</option>
7199
+ <option value="review">Under Review</option>
7200
+ <option value="scheduled">Scheduled</option>
7201
+ <option value="archived">Archived</option>
7202
+ </select>
7203
+ </div>
7204
+ </div>
7205
+ </div>
7206
+
7207
+ <!-- Actions -->
7208
+ <div class="flex items-center justify-end gap-3 pt-4 border-t border-zinc-200 dark:border-zinc-800">
7209
+ <button
7210
+ type="button"
7211
+ onclick="closeAdvancedSearch()"
7212
+ class="inline-flex items-center justify-center rounded-lg bg-white dark:bg-zinc-800 px-4 py-2 text-sm font-semibold text-zinc-950 dark:text-white ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 hover:bg-zinc-50 dark:hover:bg-zinc-700"
7213
+ >
7214
+ Cancel
7215
+ </button>
7216
+ <button
7217
+ type="submit"
7218
+ class="inline-flex items-center justify-center rounded-lg bg-indigo-600 text-white px-6 py-2.5 text-sm font-semibold hover:bg-indigo-500 shadow-sm"
7219
+ >
7220
+ Search
7221
+ </button>
7222
+ </div>
7223
+ </form>
7224
+ </div>
7225
+
7226
+ <!-- Results Area -->
7227
+ <div id="searchResults" class="hidden px-4 pb-4 sm:px-6">
7228
+ <div class="border-t border-zinc-200 dark:border-zinc-800 pt-4">
7229
+ <div id="searchResultsContent" class="space-y-3"></div>
7230
+ <div id="searchResultsPagination" class="mt-4 flex items-center justify-between"></div>
7231
+ </div>
7232
+ </div>
7233
+ </div>
7234
+ </div>
7235
+ </div>
7236
+
7237
+ <script>
7238
+ // Open modal
7239
+ function openAdvancedSearch() {
7240
+ document.getElementById('advancedSearchModal').classList.remove('hidden');
7241
+ document.getElementById('searchQuery').focus();
7242
+ }
7243
+
7244
+ // Close modal
7245
+ function closeAdvancedSearch() {
7246
+ document.getElementById('advancedSearchModal').classList.add('hidden');
7247
+ document.getElementById('searchResults').classList.add('hidden');
7248
+ }
7249
+
7250
+ // Autocomplete
7251
+ let autocompleteTimeout;
7252
+ const searchQueryInput = document.getElementById('searchQuery');
7253
+ if (searchQueryInput) {
7254
+ searchQueryInput.addEventListener('input', (e) => {
7255
+ const query = e.target.value.trim();
7256
+ const suggestionsDiv = document.getElementById('searchSuggestions');
7257
+
7258
+ clearTimeout(autocompleteTimeout);
7259
+
7260
+ if (query.length < 2) {
7261
+ suggestionsDiv.classList.add('hidden');
7262
+ return;
7263
+ }
7264
+
7265
+ autocompleteTimeout = setTimeout(async () => {
7266
+ try {
7267
+ const res = await fetch(\`/api/search/suggest?q=\${encodeURIComponent(query)}\`);
7268
+ const { data } = await res.json();
7269
+
7270
+ if (data && data.length > 0) {
7271
+ suggestionsDiv.innerHTML = data.map(s => \`
7272
+ <div class="px-4 py-2 hover:bg-zinc-100 dark:hover:bg-zinc-700 cursor-pointer" onclick="selectSuggestion('\${s.replace(/'/g, "\\'")}')">\${s}</div>
7273
+ \`).join('');
7274
+ suggestionsDiv.classList.remove('hidden');
7275
+ } else {
7276
+ suggestionsDiv.classList.add('hidden');
7277
+ }
7278
+ } catch (error) {
7279
+ console.error('Autocomplete error:', error);
7280
+ }
7281
+ }, 300);
7282
+ });
7283
+ }
7284
+
7285
+ function selectSuggestion(suggestion) {
7286
+ document.getElementById('searchQuery').value = suggestion;
7287
+ document.getElementById('searchSuggestions').classList.add('hidden');
7288
+ }
7289
+
7290
+ // Hide suggestions when clicking outside
7291
+ document.addEventListener('click', (e) => {
7292
+ const suggestionsDiv = document.getElementById('searchSuggestions');
7293
+ if (!e.target.closest('#searchQuery') && !e.target.closest('#searchSuggestions')) {
7294
+ suggestionsDiv.classList.add('hidden');
7295
+ }
7296
+ });
7297
+
7298
+ // Form submission
7299
+ const advancedSearchForm = document.getElementById('advancedSearchForm');
7300
+ if (advancedSearchForm) {
7301
+ advancedSearchForm.addEventListener('submit', async (e) => {
7302
+ e.preventDefault();
7303
+
7304
+ const formData = new FormData(e.target);
7305
+ const query = formData.get('query');
7306
+ const mode = formData.get('mode') || 'ai';
7307
+
7308
+ // Build filters
7309
+ const filters = {};
7310
+
7311
+ const collections = Array.from(formData.getAll('collections')).filter(c => c !== '');
7312
+ if (collections.length > 0) {
7313
+ // Need to convert collection names to IDs - for now, pass names
7314
+ filters.collections = collections;
7315
+ }
7316
+
7317
+ const status = Array.from(formData.getAll('status'));
7318
+ if (status.length > 0) {
7319
+ filters.status = status;
7320
+ }
7321
+
7322
+ const dateStart = formData.get('date_start');
7323
+ const dateEnd = formData.get('date_end');
7324
+ if (dateStart || dateEnd) {
7325
+ filters.dateRange = {
7326
+ start: dateStart ? new Date(dateStart) : null,
7327
+ end: dateEnd ? new Date(dateEnd) : null,
7328
+ field: 'created_at'
7329
+ };
7330
+ }
7331
+
7332
+ // Execute search
7333
+ try {
7334
+ const res = await fetch('/api/search', {
7335
+ method: 'POST',
7336
+ headers: {'Content-Type': 'application/json'},
7337
+ body: JSON.stringify({
7338
+ query,
7339
+ mode,
7340
+ filters,
7341
+ limit: 20
7342
+ })
7343
+ });
5764
7344
 
5765
- fetch('/admin/content/bulk-action', {
5766
- method: 'POST',
5767
- headers: {
5768
- 'Content-Type': 'application/json'
5769
- },
5770
- body: JSON.stringify({
5771
- action: currentBulkAction,
5772
- ids: currentSelectedIds
5773
- })
5774
- })
5775
- .then(res => res.json())
5776
- .then(data => {
5777
- if (data.success) {
5778
- location.reload();
5779
- } else {
5780
- alert('Error: ' + (data.error || 'Unknown error'));
7345
+ const { data } = await res.json();
7346
+
7347
+ if (data && data.results) {
7348
+ displaySearchResults(data);
7349
+ }
7350
+ } catch (error) {
7351
+ console.error('Search error:', error);
7352
+ alert('Search failed. Please try again.');
5781
7353
  }
5782
- })
5783
- .catch(err => {
5784
- console.error('Bulk action error:', err);
5785
- alert('Failed to perform bulk action');
5786
- })
5787
- .finally(() => {
5788
- // Clear context
5789
- currentBulkAction = null;
5790
- currentSelectedIds = [];
5791
7354
  });
5792
7355
  }
5793
7356
 
5794
- // Helper to get action text for display
5795
- function getActionText(action) {
5796
- const actionCount = currentSelectedIds.length;
5797
- switch(action) {
5798
- case 'publish':
5799
- return \`publish \${actionCount} item\${actionCount > 1 ? 's' : ''}\`;
5800
- case 'draft':
5801
- return \`move \${actionCount} item\${actionCount > 1 ? 's' : ''} to draft\`;
5802
- case 'delete':
5803
- return \`delete \${actionCount} item\${actionCount > 1 ? 's' : ''}\`;
5804
- default:
5805
- return \`perform action on \${actionCount} item\${actionCount > 1 ? 's' : ''}\`;
7357
+ function displaySearchResults(searchData) {
7358
+ const resultsDiv = document.getElementById('searchResultsContent');
7359
+ const resultsSection = document.getElementById('searchResults');
7360
+
7361
+ if (searchData.results.length === 0) {
7362
+ resultsDiv.innerHTML = '<p class="text-sm text-zinc-500 dark:text-zinc-400">No results found.</p>';
7363
+ } else {
7364
+ resultsDiv.innerHTML = searchData.results.map(result => \`
7365
+ <div class="p-4 rounded-lg border border-zinc-200 dark:border-zinc-800 hover:bg-zinc-50 dark:hover:bg-zinc-800">
7366
+ <div class="flex items-start justify-between">
7367
+ <div class="flex-1">
7368
+ <h4 class="text-sm font-semibold text-zinc-950 dark:text-white mb-1">
7369
+ <a href="/admin/content/\${result.id}/edit" class="hover:text-indigo-600 dark:hover:text-indigo-400">\${result.title || 'Untitled'}</a>
7370
+ </h4>
7371
+ <p class="text-xs text-zinc-500 dark:text-zinc-400 mb-2">
7372
+ \${result.collection_name} \u2022 \${new Date(result.created_at).toLocaleDateString()}
7373
+ \${result.relevance_score ? \` \u2022 Relevance: \${(result.relevance_score * 100).toFixed(0)}%\` : ''}
7374
+ </p>
7375
+ \${result.snippet ? \`<p class="text-sm text-zinc-600 dark:text-zinc-400">\${result.snippet}</p>\` : ''}
7376
+ </div>
7377
+ <div class="ml-4">
7378
+ <span class="px-2 py-1 text-xs rounded-full \${result.status === 'published' ? 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300' : 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-300'}">\${result.status}</span>
7379
+ </div>
7380
+ </div>
7381
+ </div>
7382
+ \`).join('');
5806
7383
  }
7384
+
7385
+ resultsSection.classList.remove('hidden');
7386
+ resultsSection.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
5807
7387
  }
5808
7388
 
7389
+ // Make functions globally available
7390
+ window.openAdvancedSearch = openAdvancedSearch;
7391
+ window.closeAdvancedSearch = closeAdvancedSearch;
5809
7392
  </script>
5810
-
5811
- <!-- Confirmation Dialog for Bulk Actions -->
5812
- ${chunkAZLU3ROK_cjs.renderConfirmationDialog({
5813
- id: "bulk-action-confirm",
5814
- title: "Confirm Bulk Action",
5815
- message: "Are you sure you want to perform this action? This operation will affect multiple items.",
5816
- confirmText: "Confirm",
5817
- cancelText: "Cancel",
5818
- confirmClass: "bg-blue-500 hover:bg-blue-400",
5819
- iconColor: "blue",
5820
- onConfirm: "executeBulkAction()"
5821
- })}
5822
-
5823
- <!-- Confirmation Dialog Script -->
5824
- ${chunkAZLU3ROK_cjs.getConfirmationDialogScript()}
5825
7393
  `;
5826
7394
  const layoutData = {
5827
7395
  title: "Content Management",
@@ -5831,7 +7399,7 @@ function renderContentListPage(data) {
5831
7399
  version: data.version,
5832
7400
  content: pageContent
5833
7401
  };
5834
- return chunkAZLU3ROK_cjs.renderAdminLayoutCatalyst(layoutData);
7402
+ return chunkBZC4FYW7_cjs.renderAdminLayoutCatalyst(layoutData);
5835
7403
  }
5836
7404
 
5837
7405
  // src/templates/components/version-history.template.ts
@@ -6025,7 +7593,123 @@ async function isPluginActive2(db, pluginId) {
6025
7593
 
6026
7594
  // src/routes/admin-content.ts
6027
7595
  var adminContentRoutes = new hono.Hono();
6028
- adminContentRoutes.use("*", chunk7I5INVNR_cjs.requireAuth());
7596
+ function parseFieldValue(field, formData, options = {}) {
7597
+ const { skipValidation = false } = options;
7598
+ const value = formData.get(field.field_name);
7599
+ const errors = [];
7600
+ const blocksConfig = chunkYMTTGHEK_cjs.getBlocksFieldConfig(field.field_options);
7601
+ if (blocksConfig) {
7602
+ const parsed = chunkYMTTGHEK_cjs.parseBlocksValue(value, blocksConfig);
7603
+ if (!skipValidation && field.is_required && parsed.value.length === 0) {
7604
+ parsed.errors.push(`${field.field_label} is required`);
7605
+ }
7606
+ return { value: parsed.value, errors: parsed.errors };
7607
+ }
7608
+ if (!skipValidation && field.is_required && (!value || value.toString().trim() === "")) {
7609
+ return { value: null, errors: [`${field.field_label} is required`] };
7610
+ }
7611
+ switch (field.field_type) {
7612
+ case "number":
7613
+ if (value && isNaN(Number(value))) {
7614
+ if (!skipValidation) {
7615
+ errors.push(`${field.field_label} must be a valid number`);
7616
+ }
7617
+ return { value: null, errors };
7618
+ }
7619
+ return { value: value ? Number(value) : null, errors: [] };
7620
+ case "boolean":
7621
+ const submitted = formData.get(`${field.field_name}_submitted`);
7622
+ return { value: submitted ? value === "true" : false, errors: [] };
7623
+ case "select":
7624
+ if (field.field_options?.multiple) {
7625
+ return { value: formData.getAll(`${field.field_name}[]`), errors: [] };
7626
+ }
7627
+ return { value, errors: [] };
7628
+ case "array": {
7629
+ if (!value || value.toString().trim() === "") {
7630
+ if (!skipValidation && field.is_required) {
7631
+ errors.push(`${field.field_label} is required`);
7632
+ }
7633
+ return { value: [], errors };
7634
+ }
7635
+ try {
7636
+ const parsed = JSON.parse(value.toString());
7637
+ if (!Array.isArray(parsed)) {
7638
+ if (!skipValidation) {
7639
+ errors.push(`${field.field_label} must be a JSON array`);
7640
+ }
7641
+ return { value: [], errors };
7642
+ }
7643
+ if (!skipValidation && field.is_required && parsed.length === 0) {
7644
+ errors.push(`${field.field_label} is required`);
7645
+ }
7646
+ return { value: parsed, errors };
7647
+ } catch {
7648
+ if (!skipValidation) {
7649
+ errors.push(`${field.field_label} must be valid JSON`);
7650
+ }
7651
+ return { value: [], errors };
7652
+ }
7653
+ }
7654
+ case "object": {
7655
+ if (!value || value.toString().trim() === "") {
7656
+ if (!skipValidation && field.is_required) {
7657
+ errors.push(`${field.field_label} is required`);
7658
+ }
7659
+ return { value: {}, errors };
7660
+ }
7661
+ try {
7662
+ const parsed = JSON.parse(value.toString());
7663
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
7664
+ if (!skipValidation) {
7665
+ errors.push(`${field.field_label} must be a JSON object`);
7666
+ }
7667
+ return { value: {}, errors };
7668
+ }
7669
+ if (!skipValidation && field.is_required && Object.keys(parsed).length === 0) {
7670
+ errors.push(`${field.field_label} is required`);
7671
+ }
7672
+ return { value: parsed, errors };
7673
+ } catch {
7674
+ if (!skipValidation) {
7675
+ errors.push(`${field.field_label} must be valid JSON`);
7676
+ }
7677
+ return { value: {}, errors };
7678
+ }
7679
+ }
7680
+ case "json": {
7681
+ if (!value || value.toString().trim() === "") {
7682
+ if (!skipValidation && field.is_required) {
7683
+ errors.push(`${field.field_label} is required`);
7684
+ }
7685
+ return { value: null, errors };
7686
+ }
7687
+ try {
7688
+ return { value: JSON.parse(value.toString()), errors: [] };
7689
+ } catch {
7690
+ if (!skipValidation) {
7691
+ errors.push(`${field.field_label} must be valid JSON`);
7692
+ }
7693
+ return { value: null, errors };
7694
+ }
7695
+ }
7696
+ default:
7697
+ return { value, errors: [] };
7698
+ }
7699
+ }
7700
+ function extractFieldData(fields, formData, options = {}) {
7701
+ const data = {};
7702
+ const errors = {};
7703
+ for (const field of fields) {
7704
+ const result = parseFieldValue(field, formData, options);
7705
+ data[field.field_name] = result.value;
7706
+ if (result.errors.length > 0) {
7707
+ errors[field.field_name] = result.errors;
7708
+ }
7709
+ }
7710
+ return { data, errors };
7711
+ }
7712
+ adminContentRoutes.use("*", chunkT3YIKW2A_cjs.requireAuth());
6029
7713
  async function getCollectionFields(db, collectionId) {
6030
7714
  const cache = chunk7FOAMNTI_cjs.getCacheService(chunk7FOAMNTI_cjs.CACHE_CONFIGS.collection);
6031
7715
  return cache.getOrSet(
@@ -6307,21 +7991,21 @@ adminContentRoutes.get("/new", async (c) => {
6307
7991
  const tinymceEnabled = await isPluginActive2(db, "tinymce-plugin");
6308
7992
  let tinymceSettings;
6309
7993
  if (tinymceEnabled) {
6310
- const pluginService = new chunkILZ3DP4I_cjs.PluginService(db);
7994
+ const pluginService = new chunkMPT5PA6U_cjs.PluginService(db);
6311
7995
  const tinymcePlugin2 = await pluginService.getPlugin("tinymce-plugin");
6312
7996
  tinymceSettings = tinymcePlugin2?.settings;
6313
7997
  }
6314
7998
  const quillEnabled = await isPluginActive2(db, "quill-editor");
6315
7999
  let quillSettings;
6316
8000
  if (quillEnabled) {
6317
- const pluginService = new chunkILZ3DP4I_cjs.PluginService(db);
8001
+ const pluginService = new chunkMPT5PA6U_cjs.PluginService(db);
6318
8002
  const quillPlugin = await pluginService.getPlugin("quill-editor");
6319
8003
  quillSettings = quillPlugin?.settings;
6320
8004
  }
6321
8005
  const mdxeditorEnabled = await isPluginActive2(db, "easy-mdx");
6322
8006
  let mdxeditorSettings;
6323
8007
  if (mdxeditorEnabled) {
6324
- const pluginService = new chunkILZ3DP4I_cjs.PluginService(db);
8008
+ const pluginService = new chunkMPT5PA6U_cjs.PluginService(db);
6325
8009
  const mdxeditorPlugin = await pluginService.getPlugin("easy-mdx");
6326
8010
  mdxeditorSettings = mdxeditorPlugin?.settings;
6327
8011
  }
@@ -6412,21 +8096,21 @@ adminContentRoutes.get("/:id/edit", async (c) => {
6412
8096
  const tinymceEnabled = await isPluginActive2(db, "tinymce-plugin");
6413
8097
  let tinymceSettings;
6414
8098
  if (tinymceEnabled) {
6415
- const pluginService = new chunkILZ3DP4I_cjs.PluginService(db);
8099
+ const pluginService = new chunkMPT5PA6U_cjs.PluginService(db);
6416
8100
  const tinymcePlugin2 = await pluginService.getPlugin("tinymce-plugin");
6417
8101
  tinymceSettings = tinymcePlugin2?.settings;
6418
8102
  }
6419
8103
  const quillEnabled = await isPluginActive2(db, "quill-editor");
6420
8104
  let quillSettings;
6421
8105
  if (quillEnabled) {
6422
- const pluginService = new chunkILZ3DP4I_cjs.PluginService(db);
8106
+ const pluginService = new chunkMPT5PA6U_cjs.PluginService(db);
6423
8107
  const quillPlugin = await pluginService.getPlugin("quill-editor");
6424
8108
  quillSettings = quillPlugin?.settings;
6425
8109
  }
6426
8110
  const mdxeditorEnabled = await isPluginActive2(db, "easy-mdx");
6427
8111
  let mdxeditorSettings;
6428
8112
  if (mdxeditorEnabled) {
6429
- const pluginService = new chunkILZ3DP4I_cjs.PluginService(db);
8113
+ const pluginService = new chunkMPT5PA6U_cjs.PluginService(db);
6430
8114
  const mdxeditorPlugin = await pluginService.getPlugin("easy-mdx");
6431
8115
  mdxeditorSettings = mdxeditorPlugin?.settings;
6432
8116
  }
@@ -6498,36 +8182,7 @@ adminContentRoutes.post("/", async (c) => {
6498
8182
  `);
6499
8183
  }
6500
8184
  const fields = await getCollectionFields(db, collectionId);
6501
- const data = {};
6502
- const errors = {};
6503
- for (const field of fields) {
6504
- const value = formData.get(field.field_name);
6505
- if (field.is_required && (!value || value.toString().trim() === "")) {
6506
- errors[field.field_name] = [`${field.field_label} is required`];
6507
- continue;
6508
- }
6509
- switch (field.field_type) {
6510
- case "number":
6511
- if (value && isNaN(Number(value))) {
6512
- errors[field.field_name] = [`${field.field_label} must be a valid number`];
6513
- } else {
6514
- data[field.field_name] = value ? Number(value) : null;
6515
- }
6516
- break;
6517
- case "boolean":
6518
- data[field.field_name] = formData.get(`${field.field_name}_submitted`) ? value === "true" : false;
6519
- break;
6520
- case "select":
6521
- if (field.field_options?.multiple) {
6522
- data[field.field_name] = formData.getAll(`${field.field_name}[]`);
6523
- } else {
6524
- data[field.field_name] = value;
6525
- }
6526
- break;
6527
- default:
6528
- data[field.field_name] = value;
6529
- }
6530
- }
8185
+ const { data, errors } = extractFieldData(fields, formData);
6531
8186
  if (Object.keys(errors).length > 0) {
6532
8187
  const formDataWithErrors = {
6533
8188
  collection,
@@ -6644,36 +8299,7 @@ adminContentRoutes.put("/:id", async (c) => {
6644
8299
  `);
6645
8300
  }
6646
8301
  const fields = await getCollectionFields(db, existingContent.collection_id);
6647
- const data = {};
6648
- const errors = {};
6649
- for (const field of fields) {
6650
- const value = formData.get(field.field_name);
6651
- if (field.is_required && (!value || value.toString().trim() === "")) {
6652
- errors[field.field_name] = [`${field.field_label} is required`];
6653
- continue;
6654
- }
6655
- switch (field.field_type) {
6656
- case "number":
6657
- if (value && isNaN(Number(value))) {
6658
- errors[field.field_name] = [`${field.field_label} must be a valid number`];
6659
- } else {
6660
- data[field.field_name] = value ? Number(value) : null;
6661
- }
6662
- break;
6663
- case "boolean":
6664
- data[field.field_name] = formData.get(`${field.field_name}_submitted`) ? value === "true" : false;
6665
- break;
6666
- case "select":
6667
- if (field.field_options?.multiple) {
6668
- data[field.field_name] = formData.getAll(`${field.field_name}[]`);
6669
- } else {
6670
- data[field.field_name] = value;
6671
- }
6672
- break;
6673
- default:
6674
- data[field.field_name] = value;
6675
- }
6676
- }
8302
+ const { data, errors } = extractFieldData(fields, formData);
6677
8303
  if (Object.keys(errors).length > 0) {
6678
8304
  const formDataWithErrors = {
6679
8305
  id,
@@ -6786,27 +8412,7 @@ adminContentRoutes.post("/preview", async (c) => {
6786
8412
  return c.html("<p>Collection not found</p>");
6787
8413
  }
6788
8414
  const fields = await getCollectionFields(db, collectionId);
6789
- const data = {};
6790
- for (const field of fields) {
6791
- const value = formData.get(field.field_name);
6792
- switch (field.field_type) {
6793
- case "number":
6794
- data[field.field_name] = value ? Number(value) : null;
6795
- break;
6796
- case "boolean":
6797
- data[field.field_name] = value === "true";
6798
- break;
6799
- case "select":
6800
- if (field.field_options?.multiple) {
6801
- data[field.field_name] = formData.getAll(`${field.field_name}[]`);
6802
- } else {
6803
- data[field.field_name] = value;
6804
- }
6805
- break;
6806
- default:
6807
- data[field.field_name] = value;
6808
- }
6809
- }
8415
+ const { data } = extractFieldData(fields, formData, { skipValidation: true });
6810
8416
  const previewHTML = `
6811
8417
  <!DOCTYPE html>
6812
8418
  <html lang="en">
@@ -7234,7 +8840,7 @@ ${JSON.stringify(data, null, 2)}
7234
8840
  var admin_content_default = adminContentRoutes;
7235
8841
 
7236
8842
  // src/templates/pages/admin-profile.template.ts
7237
- chunkAZLU3ROK_cjs.init_admin_layout_catalyst_template();
8843
+ chunkBZC4FYW7_cjs.init_admin_layout_catalyst_template();
7238
8844
  function renderAvatarImage(avatarUrl, firstName, lastName) {
7239
8845
  return `<div id="avatar-image-container" class="w-24 h-24 rounded-full mx-auto mb-4 overflow-hidden bg-gradient-to-br from-cyan-400 to-purple-400 flex items-center justify-center ring-4 ring-zinc-950/5 dark:ring-white/10">
7240
8846
  ${avatarUrl ? `<img src="${avatarUrl}" alt="Profile picture" class="w-full h-full object-cover">` : `<span class="text-2xl font-bold text-white">${firstName.charAt(0)}${lastName.charAt(0)}</span>`}
@@ -7254,8 +8860,8 @@ function renderProfilePage(data) {
7254
8860
  </div>
7255
8861
 
7256
8862
  <!-- Alert Messages -->
7257
- ${data.error ? chunkAZLU3ROK_cjs.renderAlert({ type: "error", message: data.error, dismissible: true }) : ""}
7258
- ${data.success ? chunkAZLU3ROK_cjs.renderAlert({ type: "success", message: data.success, dismissible: true }) : ""}
8863
+ ${data.error ? chunkBZC4FYW7_cjs.renderAlert({ type: "error", message: data.error, dismissible: true }) : ""}
8864
+ ${data.success ? chunkBZC4FYW7_cjs.renderAlert({ type: "success", message: data.success, dismissible: true }) : ""}
7259
8865
 
7260
8866
  <!-- Profile Form -->
7261
8867
  <div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
@@ -7642,7 +9248,7 @@ function renderProfilePage(data) {
7642
9248
  version: data.version,
7643
9249
  content: pageContent
7644
9250
  };
7645
- return chunkAZLU3ROK_cjs.renderAdminLayoutCatalyst(layoutData);
9251
+ return chunkBZC4FYW7_cjs.renderAdminLayoutCatalyst(layoutData);
7646
9252
  }
7647
9253
 
7648
9254
  // src/templates/components/alert.template.ts
@@ -7925,7 +9531,7 @@ function renderActivityLogsPage(data) {
7925
9531
  user: data.user,
7926
9532
  content: pageContent
7927
9533
  };
7928
- return chunkAZLU3ROK_cjs.renderAdminLayout(layoutData);
9534
+ return chunkBZC4FYW7_cjs.renderAdminLayout(layoutData);
7929
9535
  }
7930
9536
  function getActionBadgeClass(action) {
7931
9537
  if (action.includes("login") || action.includes("logout")) {
@@ -7945,7 +9551,7 @@ function formatAction(action) {
7945
9551
  }
7946
9552
 
7947
9553
  // src/templates/pages/admin-user-edit.template.ts
7948
- chunkAZLU3ROK_cjs.init_admin_layout_catalyst_template();
9554
+ chunkBZC4FYW7_cjs.init_admin_layout_catalyst_template();
7949
9555
 
7950
9556
  // src/templates/components/confirmation-dialog.template.ts
7951
9557
  function renderConfirmationDialog2(options) {
@@ -8066,8 +9672,8 @@ function renderUserEditPage(data) {
8066
9672
 
8067
9673
  <!-- Alert Messages -->
8068
9674
  <div id="form-messages">
8069
- ${data.error ? chunkAZLU3ROK_cjs.renderAlert({ type: "error", message: data.error, dismissible: true }) : ""}
8070
- ${data.success ? chunkAZLU3ROK_cjs.renderAlert({ type: "success", message: data.success, dismissible: true }) : ""}
9675
+ ${data.error ? chunkBZC4FYW7_cjs.renderAlert({ type: "error", message: data.error, dismissible: true }) : ""}
9676
+ ${data.success ? chunkBZC4FYW7_cjs.renderAlert({ type: "success", message: data.success, dismissible: true }) : ""}
8071
9677
  </div>
8072
9678
 
8073
9679
  <!-- User Edit Form -->
@@ -8086,7 +9692,7 @@ function renderUserEditPage(data) {
8086
9692
  <input
8087
9693
  type="text"
8088
9694
  name="first_name"
8089
- value="${chunkFYEDK7K7_cjs.escapeHtml(data.userToEdit.firstName || "")}"
9695
+ value="${chunkYMTTGHEK_cjs.escapeHtml(data.userToEdit.firstName || "")}"
8090
9696
  required
8091
9697
  class="w-full rounded-lg bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-950 dark:text-white shadow-sm ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 placeholder:text-zinc-400 dark:placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-zinc-950 dark:focus:ring-white transition-shadow"
8092
9698
  />
@@ -8097,7 +9703,7 @@ function renderUserEditPage(data) {
8097
9703
  <input
8098
9704
  type="text"
8099
9705
  name="last_name"
8100
- value="${chunkFYEDK7K7_cjs.escapeHtml(data.userToEdit.lastName || "")}"
9706
+ value="${chunkYMTTGHEK_cjs.escapeHtml(data.userToEdit.lastName || "")}"
8101
9707
  required
8102
9708
  class="w-full rounded-lg bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-950 dark:text-white shadow-sm ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 placeholder:text-zinc-400 dark:placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-zinc-950 dark:focus:ring-white transition-shadow"
8103
9709
  />
@@ -8108,7 +9714,7 @@ function renderUserEditPage(data) {
8108
9714
  <input
8109
9715
  type="text"
8110
9716
  name="username"
8111
- value="${chunkFYEDK7K7_cjs.escapeHtml(data.userToEdit.username || "")}"
9717
+ value="${chunkYMTTGHEK_cjs.escapeHtml(data.userToEdit.username || "")}"
8112
9718
  required
8113
9719
  class="w-full rounded-lg bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-950 dark:text-white shadow-sm ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 placeholder:text-zinc-400 dark:placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-zinc-950 dark:focus:ring-white transition-shadow"
8114
9720
  />
@@ -8119,7 +9725,7 @@ function renderUserEditPage(data) {
8119
9725
  <input
8120
9726
  type="email"
8121
9727
  name="email"
8122
- value="${chunkFYEDK7K7_cjs.escapeHtml(data.userToEdit.email || "")}"
9728
+ value="${chunkYMTTGHEK_cjs.escapeHtml(data.userToEdit.email || "")}"
8123
9729
  required
8124
9730
  class="w-full rounded-lg bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-950 dark:text-white shadow-sm ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 placeholder:text-zinc-400 dark:placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-zinc-950 dark:focus:ring-white transition-shadow"
8125
9731
  />
@@ -8130,7 +9736,7 @@ function renderUserEditPage(data) {
8130
9736
  <input
8131
9737
  type="tel"
8132
9738
  name="phone"
8133
- value="${chunkFYEDK7K7_cjs.escapeHtml(data.userToEdit.phone || "")}"
9739
+ value="${chunkYMTTGHEK_cjs.escapeHtml(data.userToEdit.phone || "")}"
8134
9740
  class="w-full rounded-lg bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-950 dark:text-white shadow-sm ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 placeholder:text-zinc-400 dark:placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-zinc-950 dark:focus:ring-white transition-shadow"
8135
9741
  />
8136
9742
  </div>
@@ -8144,7 +9750,7 @@ function renderUserEditPage(data) {
8144
9750
  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-zinc-500/30 dark:outline-zinc-400/30 *:bg-white dark:*:bg-zinc-800 focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-zinc-500 dark:focus-visible:outline-zinc-400 sm:text-sm/6"
8145
9751
  >
8146
9752
  ${data.roles.map((role) => `
8147
- <option value="${chunkFYEDK7K7_cjs.escapeHtml(role.value)}" ${data.userToEdit.role === role.value ? "selected" : ""}>${chunkFYEDK7K7_cjs.escapeHtml(role.label)}</option>
9753
+ <option value="${chunkYMTTGHEK_cjs.escapeHtml(role.value)}" ${data.userToEdit.role === role.value ? "selected" : ""}>${chunkYMTTGHEK_cjs.escapeHtml(role.label)}</option>
8148
9754
  `).join("")}
8149
9755
  </select>
8150
9756
  <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-zinc-600 dark:text-zinc-400 sm:size-4">
@@ -8153,14 +9759,87 @@ function renderUserEditPage(data) {
8153
9759
  </div>
8154
9760
  </div>
8155
9761
  </div>
9762
+ </div>
9763
+
9764
+ <!-- Profile Information -->
9765
+ <div class="mb-8">
9766
+ <h3 class="text-base font-semibold text-zinc-950 dark:text-white mb-4">Profile Information</h3>
9767
+ <p class="text-sm text-zinc-500 dark:text-zinc-400 mb-4">Extended profile data for this user</p>
9768
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
9769
+ <div>
9770
+ <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Display Name</label>
9771
+ <input
9772
+ type="text"
9773
+ name="profile_display_name"
9774
+ value="${chunkYMTTGHEK_cjs.escapeHtml(data.userToEdit.profile?.displayName || "")}"
9775
+ placeholder="Public display name"
9776
+ class="w-full rounded-lg bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-950 dark:text-white shadow-sm ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 placeholder:text-zinc-400 dark:placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-zinc-950 dark:focus:ring-white transition-shadow"
9777
+ />
9778
+ </div>
9779
+
9780
+ <div>
9781
+ <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Company</label>
9782
+ <input
9783
+ type="text"
9784
+ name="profile_company"
9785
+ value="${chunkYMTTGHEK_cjs.escapeHtml(data.userToEdit.profile?.company || "")}"
9786
+ placeholder="Company or organization"
9787
+ class="w-full rounded-lg bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-950 dark:text-white shadow-sm ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 placeholder:text-zinc-400 dark:placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-zinc-950 dark:focus:ring-white transition-shadow"
9788
+ />
9789
+ </div>
9790
+
9791
+ <div>
9792
+ <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Job Title</label>
9793
+ <input
9794
+ type="text"
9795
+ name="profile_job_title"
9796
+ value="${chunkYMTTGHEK_cjs.escapeHtml(data.userToEdit.profile?.jobTitle || "")}"
9797
+ placeholder="Job title or role"
9798
+ class="w-full rounded-lg bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-950 dark:text-white shadow-sm ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 placeholder:text-zinc-400 dark:placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-zinc-950 dark:focus:ring-white transition-shadow"
9799
+ />
9800
+ </div>
9801
+
9802
+ <div>
9803
+ <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Website</label>
9804
+ <input
9805
+ type="url"
9806
+ name="profile_website"
9807
+ value="${chunkYMTTGHEK_cjs.escapeHtml(data.userToEdit.profile?.website || "")}"
9808
+ placeholder="https://example.com"
9809
+ class="w-full rounded-lg bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-950 dark:text-white shadow-sm ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 placeholder:text-zinc-400 dark:placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-zinc-950 dark:focus:ring-white transition-shadow"
9810
+ />
9811
+ </div>
9812
+
9813
+ <div>
9814
+ <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Location</label>
9815
+ <input
9816
+ type="text"
9817
+ name="profile_location"
9818
+ value="${chunkYMTTGHEK_cjs.escapeHtml(data.userToEdit.profile?.location || "")}"
9819
+ placeholder="City, Country"
9820
+ class="w-full rounded-lg bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-950 dark:text-white shadow-sm ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 placeholder:text-zinc-400 dark:placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-zinc-950 dark:focus:ring-white transition-shadow"
9821
+ />
9822
+ </div>
9823
+
9824
+ <div>
9825
+ <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Date of Birth</label>
9826
+ <input
9827
+ type="date"
9828
+ name="profile_date_of_birth"
9829
+ value="${data.userToEdit.profile?.dateOfBirth ? new Date(data.userToEdit.profile.dateOfBirth).toISOString().split("T")[0] : ""}"
9830
+ class="w-full rounded-lg bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-950 dark:text-white shadow-sm ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 placeholder:text-zinc-400 dark:placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-zinc-950 dark:focus:ring-white transition-shadow"
9831
+ />
9832
+ </div>
9833
+ </div>
8156
9834
 
8157
9835
  <div class="mt-6">
8158
9836
  <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Bio</label>
8159
9837
  <textarea
8160
- name="bio"
9838
+ name="profile_bio"
8161
9839
  rows="3"
9840
+ placeholder="Short bio or description"
8162
9841
  class="w-full rounded-lg bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-950 dark:text-white shadow-sm ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 placeholder:text-zinc-400 dark:placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-zinc-950 dark:focus:ring-white transition-shadow"
8163
- >${chunkFYEDK7K7_cjs.escapeHtml(data.userToEdit.bio || "")}</textarea>
9842
+ >${chunkYMTTGHEK_cjs.escapeHtml(data.userToEdit.profile?.bio || "")}</textarea>
8164
9843
  </div>
8165
9844
  </div>
8166
9845
 
@@ -8360,11 +10039,11 @@ function renderUserEditPage(data) {
8360
10039
  user: data.user,
8361
10040
  content: pageContent
8362
10041
  };
8363
- return chunkAZLU3ROK_cjs.renderAdminLayoutCatalyst(layoutData);
10042
+ return chunkBZC4FYW7_cjs.renderAdminLayoutCatalyst(layoutData);
8364
10043
  }
8365
10044
 
8366
10045
  // src/templates/pages/admin-user-new.template.ts
8367
- chunkAZLU3ROK_cjs.init_admin_layout_catalyst_template();
10046
+ chunkBZC4FYW7_cjs.init_admin_layout_catalyst_template();
8368
10047
  function renderUserNewPage(data) {
8369
10048
  const pageContent = `
8370
10049
  <div>
@@ -8403,8 +10082,8 @@ function renderUserNewPage(data) {
8403
10082
 
8404
10083
  <!-- Alert Messages -->
8405
10084
  <div id="form-messages">
8406
- ${data.error ? chunkAZLU3ROK_cjs.renderAlert({ type: "error", message: data.error, dismissible: true }) : ""}
8407
- ${data.success ? chunkAZLU3ROK_cjs.renderAlert({ type: "success", message: data.success, dismissible: true }) : ""}
10085
+ ${data.error ? chunkBZC4FYW7_cjs.renderAlert({ type: "error", message: data.error, dismissible: true }) : ""}
10086
+ ${data.success ? chunkBZC4FYW7_cjs.renderAlert({ type: "success", message: data.success, dismissible: true }) : ""}
8408
10087
  </div>
8409
10088
 
8410
10089
  <!-- User New Form -->
@@ -8648,11 +10327,11 @@ function renderUserNewPage(data) {
8648
10327
  user: data.user,
8649
10328
  content: pageContent
8650
10329
  };
8651
- return chunkAZLU3ROK_cjs.renderAdminLayoutCatalyst(layoutData);
10330
+ return chunkBZC4FYW7_cjs.renderAdminLayoutCatalyst(layoutData);
8652
10331
  }
8653
10332
 
8654
10333
  // src/templates/pages/admin-users-list.template.ts
8655
- chunkAZLU3ROK_cjs.init_admin_layout_catalyst_template();
10334
+ chunkBZC4FYW7_cjs.init_admin_layout_catalyst_template();
8656
10335
  function renderUsersListPage(data) {
8657
10336
  const columns = [
8658
10337
  {
@@ -8803,8 +10482,8 @@ function renderUsersListPage(data) {
8803
10482
  </div>
8804
10483
 
8805
10484
  <!-- Alert Messages -->
8806
- ${data.error ? chunkAZLU3ROK_cjs.renderAlert({ type: "error", message: data.error, dismissible: true }) : ""}
8807
- ${data.success ? chunkAZLU3ROK_cjs.renderAlert({ type: "success", message: data.success, dismissible: true }) : ""}
10485
+ ${data.error ? chunkBZC4FYW7_cjs.renderAlert({ type: "error", message: data.error, dismissible: true }) : ""}
10486
+ ${data.success ? chunkBZC4FYW7_cjs.renderAlert({ type: "success", message: data.success, dismissible: true }) : ""}
8808
10487
 
8809
10488
  <!-- Stats -->
8810
10489
  <div class="mb-6">
@@ -8981,10 +10660,10 @@ function renderUsersListPage(data) {
8981
10660
  </div>
8982
10661
 
8983
10662
  <!-- Users Table -->
8984
- ${chunkAZLU3ROK_cjs.renderTable(tableData)}
10663
+ ${chunkBZC4FYW7_cjs.renderTable(tableData)}
8985
10664
 
8986
10665
  <!-- Pagination -->
8987
- ${data.pagination ? chunkAZLU3ROK_cjs.renderPagination(data.pagination) : ""}
10666
+ ${data.pagination ? chunkBZC4FYW7_cjs.renderPagination(data.pagination) : ""}
8988
10667
  </div>
8989
10668
 
8990
10669
  <script>
@@ -9055,12 +10734,12 @@ function renderUsersListPage(data) {
9055
10734
  version: data.version,
9056
10735
  content: pageContent
9057
10736
  };
9058
- return chunkAZLU3ROK_cjs.renderAdminLayoutCatalyst(layoutData);
10737
+ return chunkBZC4FYW7_cjs.renderAdminLayoutCatalyst(layoutData);
9059
10738
  }
9060
10739
 
9061
10740
  // src/routes/admin-users.ts
9062
10741
  var userRoutes = new hono.Hono();
9063
- userRoutes.use("*", chunk7I5INVNR_cjs.requireAuth());
10742
+ userRoutes.use("*", chunkT3YIKW2A_cjs.requireAuth());
9064
10743
  userRoutes.get("/", (c) => {
9065
10744
  return c.redirect("/admin/dashboard");
9066
10745
  });
@@ -9159,12 +10838,12 @@ userRoutes.put("/profile", async (c) => {
9159
10838
  const db = c.env.DB;
9160
10839
  try {
9161
10840
  const formData = await c.req.formData();
9162
- const firstName = chunkFYEDK7K7_cjs.sanitizeInput(formData.get("first_name")?.toString());
9163
- const lastName = chunkFYEDK7K7_cjs.sanitizeInput(formData.get("last_name")?.toString());
9164
- const username = chunkFYEDK7K7_cjs.sanitizeInput(formData.get("username")?.toString());
10841
+ const firstName = chunkYMTTGHEK_cjs.sanitizeInput(formData.get("first_name")?.toString());
10842
+ const lastName = chunkYMTTGHEK_cjs.sanitizeInput(formData.get("last_name")?.toString());
10843
+ const username = chunkYMTTGHEK_cjs.sanitizeInput(formData.get("username")?.toString());
9165
10844
  const email = formData.get("email")?.toString()?.trim().toLowerCase() || "";
9166
- const phone = chunkFYEDK7K7_cjs.sanitizeInput(formData.get("phone")?.toString()) || null;
9167
- const bio = chunkFYEDK7K7_cjs.sanitizeInput(formData.get("bio")?.toString()) || null;
10845
+ const phone = chunkYMTTGHEK_cjs.sanitizeInput(formData.get("phone")?.toString()) || null;
10846
+ const bio = chunkYMTTGHEK_cjs.sanitizeInput(formData.get("bio")?.toString()) || null;
9168
10847
  const timezone = formData.get("timezone")?.toString() || "UTC";
9169
10848
  const language = formData.get("language")?.toString() || "en";
9170
10849
  const emailNotifications = formData.get("email_notifications") === "1";
@@ -9215,7 +10894,7 @@ userRoutes.put("/profile", async (c) => {
9215
10894
  Date.now(),
9216
10895
  user.userId
9217
10896
  ).run();
9218
- await chunk7I5INVNR_cjs.logActivity(
10897
+ await chunkT3YIKW2A_cjs.logActivity(
9219
10898
  db,
9220
10899
  user.userId,
9221
10900
  "profile.update",
@@ -9278,7 +10957,7 @@ userRoutes.post("/profile/avatar", async (c) => {
9278
10957
  SELECT first_name, last_name FROM users WHERE id = ?
9279
10958
  `);
9280
10959
  const userData = await userStmt.bind(user.userId).first();
9281
- await chunk7I5INVNR_cjs.logActivity(
10960
+ await chunkT3YIKW2A_cjs.logActivity(
9282
10961
  db,
9283
10962
  user.userId,
9284
10963
  "profile.avatar_update",
@@ -9349,7 +11028,7 @@ userRoutes.post("/profile/password", async (c) => {
9349
11028
  dismissible: true
9350
11029
  }));
9351
11030
  }
9352
- const validPassword = await chunk7I5INVNR_cjs.AuthManager.verifyPassword(currentPassword, userData.password_hash);
11031
+ const validPassword = await chunkT3YIKW2A_cjs.AuthManager.verifyPassword(currentPassword, userData.password_hash);
9353
11032
  if (!validPassword) {
9354
11033
  return c.html(renderAlert2({
9355
11034
  type: "error",
@@ -9357,7 +11036,7 @@ userRoutes.post("/profile/password", async (c) => {
9357
11036
  dismissible: true
9358
11037
  }));
9359
11038
  }
9360
- const newPasswordHash = await chunk7I5INVNR_cjs.AuthManager.hashPassword(newPassword);
11039
+ const newPasswordHash = await chunkT3YIKW2A_cjs.AuthManager.hashPassword(newPassword);
9361
11040
  const historyStmt = db.prepare(`
9362
11041
  INSERT INTO password_history (id, user_id, password_hash, created_at)
9363
11042
  VALUES (?, ?, ?, ?)
@@ -9373,7 +11052,7 @@ userRoutes.post("/profile/password", async (c) => {
9373
11052
  WHERE id = ?
9374
11053
  `);
9375
11054
  await updateStmt.bind(newPasswordHash, Date.now(), user.userId).run();
9376
- await chunk7I5INVNR_cjs.logActivity(
11055
+ await chunkT3YIKW2A_cjs.logActivity(
9377
11056
  db,
9378
11057
  user.userId,
9379
11058
  "profile.password_change",
@@ -9440,7 +11119,7 @@ userRoutes.get("/users", async (c) => {
9440
11119
  `);
9441
11120
  const countResult = await countStmt.bind(...params).first();
9442
11121
  const totalUsers = countResult?.total || 0;
9443
- await chunk7I5INVNR_cjs.logActivity(
11122
+ await chunkT3YIKW2A_cjs.logActivity(
9444
11123
  db,
9445
11124
  user.userId,
9446
11125
  "users.list_view",
@@ -9542,12 +11221,12 @@ userRoutes.post("/users/new", async (c) => {
9542
11221
  const user = c.get("user");
9543
11222
  try {
9544
11223
  const formData = await c.req.formData();
9545
- const firstName = chunkFYEDK7K7_cjs.sanitizeInput(formData.get("first_name")?.toString());
9546
- const lastName = chunkFYEDK7K7_cjs.sanitizeInput(formData.get("last_name")?.toString());
9547
- const username = chunkFYEDK7K7_cjs.sanitizeInput(formData.get("username")?.toString());
11224
+ const firstName = chunkYMTTGHEK_cjs.sanitizeInput(formData.get("first_name")?.toString());
11225
+ const lastName = chunkYMTTGHEK_cjs.sanitizeInput(formData.get("last_name")?.toString());
11226
+ const username = chunkYMTTGHEK_cjs.sanitizeInput(formData.get("username")?.toString());
9548
11227
  const email = formData.get("email")?.toString()?.trim().toLowerCase() || "";
9549
- const phone = chunkFYEDK7K7_cjs.sanitizeInput(formData.get("phone")?.toString()) || null;
9550
- const bio = chunkFYEDK7K7_cjs.sanitizeInput(formData.get("bio")?.toString()) || null;
11228
+ const phone = chunkYMTTGHEK_cjs.sanitizeInput(formData.get("phone")?.toString()) || null;
11229
+ const bio = chunkYMTTGHEK_cjs.sanitizeInput(formData.get("bio")?.toString()) || null;
9551
11230
  const role = formData.get("role")?.toString() || "viewer";
9552
11231
  const password = formData.get("password")?.toString() || "";
9553
11232
  const confirmPassword = formData.get("confirm_password")?.toString() || "";
@@ -9594,7 +11273,7 @@ userRoutes.post("/users/new", async (c) => {
9594
11273
  dismissible: true
9595
11274
  }));
9596
11275
  }
9597
- const passwordHash = await chunk7I5INVNR_cjs.AuthManager.hashPassword(password);
11276
+ const passwordHash = await chunkT3YIKW2A_cjs.AuthManager.hashPassword(password);
9598
11277
  const userId = crypto.randomUUID();
9599
11278
  const createStmt = db.prepare(`
9600
11279
  INSERT INTO users (
@@ -9617,7 +11296,7 @@ userRoutes.post("/users/new", async (c) => {
9617
11296
  Date.now(),
9618
11297
  Date.now()
9619
11298
  ).run();
9620
- await chunk7I5INVNR_cjs.logActivity(
11299
+ await chunkT3YIKW2A_cjs.logActivity(
9621
11300
  db,
9622
11301
  user.userId,
9623
11302
  "user!.create",
@@ -9655,7 +11334,7 @@ userRoutes.get("/users/:id", async (c) => {
9655
11334
  if (!userRecord) {
9656
11335
  return c.json({ error: "User not found" }, 404);
9657
11336
  }
9658
- await chunk7I5INVNR_cjs.logActivity(
11337
+ await chunkT3YIKW2A_cjs.logActivity(
9659
11338
  db,
9660
11339
  user.userId,
9661
11340
  "user!.view",
@@ -9694,7 +11373,7 @@ userRoutes.get("/users/:id/edit", async (c) => {
9694
11373
  const userId = c.req.param("id");
9695
11374
  try {
9696
11375
  const userStmt = db.prepare(`
9697
- SELECT id, email, username, first_name, last_name, phone, bio, avatar_url,
11376
+ SELECT id, email, username, first_name, last_name, phone, avatar_url,
9698
11377
  role, is_active, email_verified, two_factor_enabled, created_at, last_login_at
9699
11378
  FROM users
9700
11379
  WHERE id = ?
@@ -9707,6 +11386,21 @@ userRoutes.get("/users/:id/edit", async (c) => {
9707
11386
  dismissible: true
9708
11387
  }), 404);
9709
11388
  }
11389
+ const profileStmt = db.prepare(`
11390
+ SELECT display_name, bio, company, job_title, website, location, date_of_birth
11391
+ FROM user_profiles
11392
+ WHERE user_id = ?
11393
+ `);
11394
+ const profileData = await profileStmt.bind(userId).first();
11395
+ const profile = profileData ? {
11396
+ displayName: profileData.display_name,
11397
+ bio: profileData.bio,
11398
+ company: profileData.company,
11399
+ jobTitle: profileData.job_title,
11400
+ website: profileData.website,
11401
+ location: profileData.location,
11402
+ dateOfBirth: profileData.date_of_birth
11403
+ } : void 0;
9710
11404
  const editData = {
9711
11405
  id: userToEdit.id,
9712
11406
  email: userToEdit.email,
@@ -9714,14 +11408,14 @@ userRoutes.get("/users/:id/edit", async (c) => {
9714
11408
  firstName: userToEdit.first_name || "",
9715
11409
  lastName: userToEdit.last_name || "",
9716
11410
  phone: userToEdit.phone,
9717
- bio: userToEdit.bio,
9718
11411
  avatarUrl: userToEdit.avatar_url,
9719
11412
  role: userToEdit.role,
9720
11413
  isActive: Boolean(userToEdit.is_active),
9721
11414
  emailVerified: Boolean(userToEdit.email_verified),
9722
11415
  twoFactorEnabled: Boolean(userToEdit.two_factor_enabled),
9723
11416
  createdAt: userToEdit.created_at,
9724
- lastLoginAt: userToEdit.last_login_at
11417
+ lastLoginAt: userToEdit.last_login_at,
11418
+ profile
9725
11419
  };
9726
11420
  const pageData = {
9727
11421
  userToEdit: editData,
@@ -9737,7 +11431,7 @@ userRoutes.get("/users/:id/edit", async (c) => {
9737
11431
  console.error("User edit page error:", error);
9738
11432
  return c.html(renderAlert2({
9739
11433
  type: "error",
9740
- message: "Failed to load user!. Please try again.",
11434
+ message: "Failed to load user. Please try again.",
9741
11435
  dismissible: true
9742
11436
  }), 500);
9743
11437
  }
@@ -9748,15 +11442,22 @@ userRoutes.put("/users/:id", async (c) => {
9748
11442
  const userId = c.req.param("id");
9749
11443
  try {
9750
11444
  const formData = await c.req.formData();
9751
- const firstName = chunkFYEDK7K7_cjs.sanitizeInput(formData.get("first_name")?.toString());
9752
- const lastName = chunkFYEDK7K7_cjs.sanitizeInput(formData.get("last_name")?.toString());
9753
- const username = chunkFYEDK7K7_cjs.sanitizeInput(formData.get("username")?.toString());
11445
+ const firstName = chunkYMTTGHEK_cjs.sanitizeInput(formData.get("first_name")?.toString());
11446
+ const lastName = chunkYMTTGHEK_cjs.sanitizeInput(formData.get("last_name")?.toString());
11447
+ const username = chunkYMTTGHEK_cjs.sanitizeInput(formData.get("username")?.toString());
9754
11448
  const email = formData.get("email")?.toString()?.trim().toLowerCase() || "";
9755
- const phone = chunkFYEDK7K7_cjs.sanitizeInput(formData.get("phone")?.toString()) || null;
9756
- const bio = chunkFYEDK7K7_cjs.sanitizeInput(formData.get("bio")?.toString()) || null;
11449
+ const phone = chunkYMTTGHEK_cjs.sanitizeInput(formData.get("phone")?.toString()) || null;
9757
11450
  const role = formData.get("role")?.toString() || "viewer";
9758
11451
  const isActive = formData.get("is_active") === "1";
9759
11452
  const emailVerified = formData.get("email_verified") === "1";
11453
+ const profileDisplayName = chunkYMTTGHEK_cjs.sanitizeInput(formData.get("profile_display_name")?.toString()) || null;
11454
+ const profileBio = chunkYMTTGHEK_cjs.sanitizeInput(formData.get("profile_bio")?.toString()) || null;
11455
+ const profileCompany = chunkYMTTGHEK_cjs.sanitizeInput(formData.get("profile_company")?.toString()) || null;
11456
+ const profileJobTitle = chunkYMTTGHEK_cjs.sanitizeInput(formData.get("profile_job_title")?.toString()) || null;
11457
+ const profileWebsite = formData.get("profile_website")?.toString()?.trim() || null;
11458
+ const profileLocation = chunkYMTTGHEK_cjs.sanitizeInput(formData.get("profile_location")?.toString()) || null;
11459
+ const profileDateOfBirthStr = formData.get("profile_date_of_birth")?.toString()?.trim() || null;
11460
+ const profileDateOfBirth = profileDateOfBirthStr ? new Date(profileDateOfBirthStr).getTime() : null;
9760
11461
  if (!firstName || !lastName || !username || !email) {
9761
11462
  return c.html(renderAlert2({
9762
11463
  type: "error",
@@ -9772,6 +11473,17 @@ userRoutes.put("/users/:id", async (c) => {
9772
11473
  dismissible: true
9773
11474
  }));
9774
11475
  }
11476
+ if (profileWebsite) {
11477
+ try {
11478
+ new URL(profileWebsite);
11479
+ } catch {
11480
+ return c.html(renderAlert2({
11481
+ type: "error",
11482
+ message: "Please enter a valid website URL.",
11483
+ dismissible: true
11484
+ }));
11485
+ }
11486
+ }
9775
11487
  const checkStmt = db.prepare(`
9776
11488
  SELECT id FROM users
9777
11489
  WHERE (username = ? OR email = ?) AND id != ?
@@ -9780,14 +11492,14 @@ userRoutes.put("/users/:id", async (c) => {
9780
11492
  if (existingUser) {
9781
11493
  return c.html(renderAlert2({
9782
11494
  type: "error",
9783
- message: "Username or email is already taken by another user!.",
11495
+ message: "Username or email is already taken by another user.",
9784
11496
  dismissible: true
9785
11497
  }));
9786
11498
  }
9787
11499
  const updateStmt = db.prepare(`
9788
11500
  UPDATE users SET
9789
11501
  first_name = ?, last_name = ?, username = ?, email = ?,
9790
- phone = ?, bio = ?, role = ?, is_active = ?, email_verified = ?,
11502
+ phone = ?, role = ?, is_active = ?, email_verified = ?,
9791
11503
  updated_at = ?
9792
11504
  WHERE id = ?
9793
11505
  `);
@@ -9797,20 +11509,63 @@ userRoutes.put("/users/:id", async (c) => {
9797
11509
  username,
9798
11510
  email,
9799
11511
  phone,
9800
- bio,
9801
11512
  role,
9802
11513
  isActive ? 1 : 0,
9803
11514
  emailVerified ? 1 : 0,
9804
11515
  Date.now(),
9805
11516
  userId
9806
11517
  ).run();
9807
- await chunk7I5INVNR_cjs.logActivity(
11518
+ const hasProfileData = profileDisplayName || profileBio || profileCompany || profileJobTitle || profileWebsite || profileLocation || profileDateOfBirth;
11519
+ if (hasProfileData) {
11520
+ const now = Date.now();
11521
+ const profileCheckStmt = db.prepare(`SELECT id FROM user_profiles WHERE user_id = ?`);
11522
+ const existingProfile = await profileCheckStmt.bind(userId).first();
11523
+ if (existingProfile) {
11524
+ const updateProfileStmt = db.prepare(`
11525
+ UPDATE user_profiles SET
11526
+ display_name = ?, bio = ?, company = ?, job_title = ?,
11527
+ website = ?, location = ?, date_of_birth = ?, updated_at = ?
11528
+ WHERE user_id = ?
11529
+ `);
11530
+ await updateProfileStmt.bind(
11531
+ profileDisplayName,
11532
+ profileBio,
11533
+ profileCompany,
11534
+ profileJobTitle,
11535
+ profileWebsite,
11536
+ profileLocation,
11537
+ profileDateOfBirth,
11538
+ now,
11539
+ userId
11540
+ ).run();
11541
+ } else {
11542
+ const profileId = `profile_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
11543
+ const insertProfileStmt = db.prepare(`
11544
+ INSERT INTO user_profiles (id, user_id, display_name, bio, company, job_title, website, location, date_of_birth, created_at, updated_at)
11545
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
11546
+ `);
11547
+ await insertProfileStmt.bind(
11548
+ profileId,
11549
+ userId,
11550
+ profileDisplayName,
11551
+ profileBio,
11552
+ profileCompany,
11553
+ profileJobTitle,
11554
+ profileWebsite,
11555
+ profileLocation,
11556
+ profileDateOfBirth,
11557
+ now,
11558
+ now
11559
+ ).run();
11560
+ }
11561
+ }
11562
+ await chunkT3YIKW2A_cjs.logActivity(
9808
11563
  db,
9809
11564
  user.userId,
9810
- "user!.update",
11565
+ "user.update",
9811
11566
  "users",
9812
11567
  userId,
9813
- { fields: ["first_name", "last_name", "username", "email", "phone", "bio", "role", "is_active", "email_verified"] },
11568
+ { fields: ["first_name", "last_name", "username", "email", "phone", "role", "is_active", "email_verified", "profile"] },
9814
11569
  c.req.header("x-forwarded-for") || c.req.header("cf-connecting-ip"),
9815
11570
  c.req.header("user-agent")
9816
11571
  );
@@ -9823,7 +11578,7 @@ userRoutes.put("/users/:id", async (c) => {
9823
11578
  console.error("User update error:", error);
9824
11579
  return c.html(renderAlert2({
9825
11580
  type: "error",
9826
- message: "Failed to update user!. Please try again.",
11581
+ message: "Failed to update user. Please try again.",
9827
11582
  dismissible: true
9828
11583
  }));
9829
11584
  }
@@ -9849,7 +11604,7 @@ userRoutes.post("/users/:id/toggle", async (c) => {
9849
11604
  UPDATE users SET is_active = ?, updated_at = ? WHERE id = ?
9850
11605
  `);
9851
11606
  await toggleStmt.bind(active ? 1 : 0, Date.now(), userId).run();
9852
- await chunk7I5INVNR_cjs.logActivity(
11607
+ await chunkT3YIKW2A_cjs.logActivity(
9853
11608
  db,
9854
11609
  user.userId,
9855
11610
  active ? "user.activate" : "user.deactivate",
@@ -9890,7 +11645,7 @@ userRoutes.delete("/users/:id", async (c) => {
9890
11645
  DELETE FROM users WHERE id = ?
9891
11646
  `);
9892
11647
  await deleteStmt.bind(userId).run();
9893
- await chunk7I5INVNR_cjs.logActivity(
11648
+ await chunkT3YIKW2A_cjs.logActivity(
9894
11649
  db,
9895
11650
  user.userId,
9896
11651
  "user!.hard_delete",
@@ -9909,7 +11664,7 @@ userRoutes.delete("/users/:id", async (c) => {
9909
11664
  UPDATE users SET is_active = 0, updated_at = ? WHERE id = ?
9910
11665
  `);
9911
11666
  await deleteStmt.bind(Date.now(), userId).run();
9912
- await chunk7I5INVNR_cjs.logActivity(
11667
+ await chunkT3YIKW2A_cjs.logActivity(
9913
11668
  db,
9914
11669
  user.userId,
9915
11670
  "user!.soft_delete",
@@ -9936,8 +11691,8 @@ userRoutes.post("/invite-user", async (c) => {
9936
11691
  const formData = await c.req.formData();
9937
11692
  const email = formData.get("email")?.toString()?.trim().toLowerCase() || "";
9938
11693
  const role = formData.get("role")?.toString()?.trim() || "viewer";
9939
- const firstName = chunkFYEDK7K7_cjs.sanitizeInput(formData.get("first_name")?.toString());
9940
- const lastName = chunkFYEDK7K7_cjs.sanitizeInput(formData.get("last_name")?.toString());
11694
+ const firstName = chunkYMTTGHEK_cjs.sanitizeInput(formData.get("first_name")?.toString());
11695
+ const lastName = chunkYMTTGHEK_cjs.sanitizeInput(formData.get("last_name")?.toString());
9941
11696
  if (!email || !firstName || !lastName) {
9942
11697
  return c.json({ error: "Email, first name, and last name are required" }, 400);
9943
11698
  }
@@ -9975,7 +11730,7 @@ userRoutes.post("/invite-user", async (c) => {
9975
11730
  Date.now(),
9976
11731
  Date.now()
9977
11732
  ).run();
9978
- await chunk7I5INVNR_cjs.logActivity(
11733
+ await chunkT3YIKW2A_cjs.logActivity(
9979
11734
  db,
9980
11735
  user.userId,
9981
11736
  "user!.invite_sent",
@@ -10032,7 +11787,7 @@ userRoutes.post("/resend-invitation/:id", async (c) => {
10032
11787
  Date.now(),
10033
11788
  userId
10034
11789
  ).run();
10035
- await chunk7I5INVNR_cjs.logActivity(
11790
+ await chunkT3YIKW2A_cjs.logActivity(
10036
11791
  db,
10037
11792
  user.userId,
10038
11793
  "user!.invitation_resent",
@@ -10068,7 +11823,7 @@ userRoutes.delete("/cancel-invitation/:id", async (c) => {
10068
11823
  }
10069
11824
  const deleteStmt = db.prepare(`DELETE FROM users WHERE id = ?`);
10070
11825
  await deleteStmt.bind(userId).run();
10071
- await chunk7I5INVNR_cjs.logActivity(
11826
+ await chunkT3YIKW2A_cjs.logActivity(
10072
11827
  db,
10073
11828
  user.userId,
10074
11829
  "user!.invitation_cancelled",
@@ -10151,7 +11906,7 @@ userRoutes.get("/activity-logs", async (c) => {
10151
11906
  ...log,
10152
11907
  details: log.details ? JSON.parse(log.details) : null
10153
11908
  }));
10154
- await chunk7I5INVNR_cjs.logActivity(
11909
+ await chunkT3YIKW2A_cjs.logActivity(
10155
11910
  db,
10156
11911
  user.userId,
10157
11912
  "activity.logs_viewed",
@@ -10258,7 +12013,7 @@ userRoutes.get("/activity-logs/export", async (c) => {
10258
12013
  csvRows.push(row.join(","));
10259
12014
  }
10260
12015
  const csvContent = csvRows.join("\n");
10261
- await chunk7I5INVNR_cjs.logActivity(
12016
+ await chunkT3YIKW2A_cjs.logActivity(
10262
12017
  db,
10263
12018
  user.userId,
10264
12019
  "activity.logs_exported",
@@ -10476,7 +12231,7 @@ function getFileIcon(mimeType) {
10476
12231
  }
10477
12232
 
10478
12233
  // src/templates/pages/admin-media-library.template.ts
10479
- chunkAZLU3ROK_cjs.init_admin_layout_catalyst_template();
12234
+ chunkBZC4FYW7_cjs.init_admin_layout_catalyst_template();
10480
12235
  function renderMediaLibraryPage(data) {
10481
12236
  const pageContent = `
10482
12237
  <div>
@@ -11411,7 +13166,7 @@ function renderMediaLibraryPage(data) {
11411
13166
  version: data.version,
11412
13167
  content: pageContent
11413
13168
  };
11414
- return chunkAZLU3ROK_cjs.renderAdminLayoutCatalyst(layoutData);
13169
+ return chunkBZC4FYW7_cjs.renderAdminLayoutCatalyst(layoutData);
11415
13170
  }
11416
13171
 
11417
13172
  // src/templates/components/media-file-details.template.ts
@@ -11597,7 +13352,7 @@ var fileValidationSchema2 = zod.z.object({
11597
13352
  // 50MB max
11598
13353
  });
11599
13354
  var adminMediaRoutes = new hono.Hono();
11600
- adminMediaRoutes.use("*", chunk7I5INVNR_cjs.requireAuth());
13355
+ adminMediaRoutes.use("*", chunkT3YIKW2A_cjs.requireAuth());
11601
13356
  adminMediaRoutes.get("/", async (c) => {
11602
13357
  try {
11603
13358
  const user = c.get("user");
@@ -12183,7 +13938,7 @@ adminMediaRoutes.put("/:id", async (c) => {
12183
13938
  `);
12184
13939
  }
12185
13940
  });
12186
- adminMediaRoutes.delete("/cleanup", chunk7I5INVNR_cjs.requireRole("admin"), async (c) => {
13941
+ adminMediaRoutes.delete("/cleanup", chunkT3YIKW2A_cjs.requireRole("admin"), async (c) => {
12187
13942
  try {
12188
13943
  const db = c.env.DB;
12189
13944
  const allMediaStmt = db.prepare("SELECT id, r2_key, filename FROM media WHERE deleted_at IS NULL");
@@ -12433,7 +14188,7 @@ function formatFileSize(bytes) {
12433
14188
  }
12434
14189
 
12435
14190
  // src/templates/pages/admin-plugins-list.template.ts
12436
- chunkAZLU3ROK_cjs.init_admin_layout_catalyst_template();
14191
+ chunkBZC4FYW7_cjs.init_admin_layout_catalyst_template();
12437
14192
  function renderPluginsListPage(data) {
12438
14193
  const categories = [
12439
14194
  { value: "content", label: "Content Management" },
@@ -12913,7 +14668,7 @@ function renderPluginsListPage(data) {
12913
14668
  version: data.version,
12914
14669
  content: pageContent
12915
14670
  };
12916
- return chunkAZLU3ROK_cjs.renderAdminLayoutCatalyst(layoutData);
14671
+ return chunkBZC4FYW7_cjs.renderAdminLayoutCatalyst(layoutData);
12917
14672
  }
12918
14673
  function renderPluginCard(plugin) {
12919
14674
  const statusColors = {
@@ -13317,6 +15072,9 @@ function renderAuthSettingsForm(settings) {
13317
15072
  }
13318
15073
 
13319
15074
  // src/templates/pages/admin-plugin-settings.template.ts
15075
+ function escapeHtmlAttr(value) {
15076
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/'/g, "&#39;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
15077
+ }
13320
15078
  function renderPluginSettingsPage(data) {
13321
15079
  const { plugin, activity = [], user } = data;
13322
15080
  const pageContent = `
@@ -13565,7 +15323,7 @@ function renderPluginSettingsPage(data) {
13565
15323
  user,
13566
15324
  content: pageContent
13567
15325
  };
13568
- return chunkAZLU3ROK_cjs.renderAdminLayout(layoutData);
15326
+ return chunkBZC4FYW7_cjs.renderAdminLayout(layoutData);
13569
15327
  }
13570
15328
  function renderStatusBadge(status) {
13571
15329
  const statusColors = {
@@ -13594,6 +15352,7 @@ function renderSettingsTab(plugin) {
13594
15352
  const settings = plugin.settings || {};
13595
15353
  const isSeedDataPlugin = plugin.id === "seed-data" || plugin.name === "seed-data";
13596
15354
  const isAuthPlugin = plugin.id === "core-auth" || plugin.name === "core-auth";
15355
+ const isTurnstilePlugin = plugin.id === "turnstile" || plugin.name === "turnstile";
13597
15356
  return `
13598
15357
  ${isSeedDataPlugin ? `
13599
15358
  <div class="backdrop-blur-md bg-black/20 rounded-xl border border-white/10 shadow-xl p-6 mb-6">
@@ -13620,12 +15379,15 @@ function renderSettingsTab(plugin) {
13620
15379
  ${isAuthPlugin ? `
13621
15380
  <h2 class="text-xl font-semibold text-white mb-4">Authentication Settings</h2>
13622
15381
  <p class="text-gray-400 mb-6">Configure user registration fields and validation rules.</p>
15382
+ ` : isTurnstilePlugin ? `
15383
+ <h2 class="text-xl font-semibold text-white mb-4">Cloudflare Turnstile Settings</h2>
15384
+ <p class="text-gray-400 mb-6">Configure CAPTCHA-free bot protection for your forms.</p>
13623
15385
  ` : `
13624
15386
  <h2 class="text-xl font-semibold text-white mb-4">Plugin Settings</h2>
13625
15387
  `}
13626
15388
 
13627
15389
  <form id="settings-form" class="space-y-6">
13628
- ${isAuthPlugin && Object.keys(settings).length > 0 ? renderAuthSettingsForm(settings) : Object.keys(settings).length > 0 ? renderSettingsFields(settings) : renderNoSettings(plugin)}
15390
+ ${isAuthPlugin && Object.keys(settings).length > 0 ? renderAuthSettingsForm(settings) : isTurnstilePlugin && Object.keys(settings).length > 0 ? renderTurnstileSettingsForm(settings) : Object.keys(settings).length > 0 ? renderSettingsFields(settings) : renderNoSettings(plugin)}
13629
15391
 
13630
15392
  ${Object.keys(settings).length > 0 ? `
13631
15393
  <div class="flex items-center justify-end pt-6 border-t border-white/10">
@@ -13689,6 +15451,80 @@ function renderSettingsFields(settings) {
13689
15451
  }
13690
15452
  }).join("");
13691
15453
  }
15454
+ function renderTurnstileSettingsForm(settings) {
15455
+ const inputClass = "backdrop-blur-sm bg-white/10 border border-white/20 rounded-lg px-3 py-2 text-white placeholder-gray-300 focus:border-blue-400 focus:outline-none transition-colors w-full";
15456
+ const selectClass = "backdrop-blur-sm bg-zinc-800 border border-white/20 rounded-lg px-3 py-2 text-white focus:border-blue-400 focus:outline-none transition-colors w-full [&>option]:bg-zinc-800 [&>option]:text-white";
15457
+ return `
15458
+ <!-- Enable Toggle -->
15459
+ <div class="flex items-center justify-between">
15460
+ <div>
15461
+ <label for="setting_enabled" class="text-sm font-medium text-gray-300">Enable Turnstile</label>
15462
+ <p class="text-xs text-gray-400">Enable or disable Turnstile verification globally</p>
15463
+ </div>
15464
+ <label class="relative inline-flex items-center cursor-pointer">
15465
+ <input type="checkbox" name="setting_enabled" id="setting_enabled" ${settings.enabled ? "checked" : ""} class="sr-only peer">
15466
+ <div class="w-11 h-6 bg-gray-600 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-800 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
15467
+ </label>
15468
+ </div>
15469
+
15470
+ <!-- Site Key -->
15471
+ <div>
15472
+ <label for="setting_siteKey" class="block text-sm font-medium text-gray-300 mb-2">Site Key</label>
15473
+ <input type="text" name="setting_siteKey" id="setting_siteKey" value="${escapeHtmlAttr(settings.siteKey || "")}" placeholder="0x4AAAAAAAA..." class="${inputClass}">
15474
+ <p class="text-xs text-gray-400 mt-1">Your Cloudflare Turnstile site key (public)</p>
15475
+ </div>
15476
+
15477
+ <!-- Secret Key -->
15478
+ <div>
15479
+ <label for="setting_secretKey" class="block text-sm font-medium text-gray-300 mb-2">Secret Key</label>
15480
+ <input type="password" name="setting_secretKey" id="setting_secretKey" value="${escapeHtmlAttr(settings.secretKey || "")}" placeholder="0x4AAAAAAAA..." class="${inputClass}">
15481
+ <p class="text-xs text-gray-400 mt-1">Your Cloudflare Turnstile secret key (private)</p>
15482
+ </div>
15483
+
15484
+ <!-- Theme -->
15485
+ <div>
15486
+ <label for="setting_theme" class="block text-sm font-medium text-gray-300 mb-2">Widget Theme</label>
15487
+ <select name="setting_theme" id="setting_theme" class="${selectClass}" style="color: white; background-color: rgb(39, 39, 42);">
15488
+ <option value="auto" ${settings.theme === "auto" ? "selected" : ""} style="background-color: rgb(39, 39, 42); color: white;">Auto (matches page theme)</option>
15489
+ <option value="light" ${settings.theme === "light" ? "selected" : ""} style="background-color: rgb(39, 39, 42); color: white;">Light</option>
15490
+ <option value="dark" ${settings.theme === "dark" ? "selected" : ""} style="background-color: rgb(39, 39, 42); color: white;">Dark</option>
15491
+ </select>
15492
+ <p class="text-xs text-gray-400 mt-1">Visual appearance of the Turnstile widget</p>
15493
+ </div>
15494
+
15495
+ <!-- Size -->
15496
+ <div>
15497
+ <label for="setting_size" class="block text-sm font-medium text-gray-300 mb-2">Widget Size</label>
15498
+ <select name="setting_size" id="setting_size" class="${selectClass}" style="color: white; background-color: rgb(39, 39, 42);">
15499
+ <option value="normal" ${settings.size === "normal" ? "selected" : ""} style="background-color: rgb(39, 39, 42); color: white;">Normal (300x65px)</option>
15500
+ <option value="compact" ${settings.size === "compact" ? "selected" : ""} style="background-color: rgb(39, 39, 42); color: white;">Compact (130x120px)</option>
15501
+ </select>
15502
+ <p class="text-xs text-gray-400 mt-1">Size of the Turnstile challenge widget</p>
15503
+ </div>
15504
+
15505
+ <!-- Widget Mode -->
15506
+ <div>
15507
+ <label for="setting_mode" class="block text-sm font-medium text-gray-300 mb-2">Widget Mode</label>
15508
+ <select name="setting_mode" id="setting_mode" class="${selectClass}" style="color: white; background-color: rgb(39, 39, 42);">
15509
+ <option value="managed" ${!settings.mode || settings.mode === "managed" ? "selected" : ""} style="background-color: rgb(39, 39, 42); color: white;">Managed (Recommended) - Adaptive challenge</option>
15510
+ <option value="non-interactive" ${settings.mode === "non-interactive" ? "selected" : ""} style="background-color: rgb(39, 39, 42); color: white;">Non-Interactive - Always visible, minimal friction</option>
15511
+ <option value="invisible" ${settings.mode === "invisible" ? "selected" : ""} style="background-color: rgb(39, 39, 42); color: white;">Invisible - No visible widget</option>
15512
+ </select>
15513
+ <p class="text-xs text-gray-400 mt-1"><strong>Managed:</strong> Shows challenge only when needed. <strong>Non-Interactive:</strong> Always shows but doesn't require interaction. <strong>Invisible:</strong> Runs in background without UI.</p>
15514
+ </div>
15515
+
15516
+ <!-- Appearance (Pre-clearance) -->
15517
+ <div>
15518
+ <label for="setting_appearance" class="block text-sm font-medium text-gray-300 mb-2">Pre-clearance / Appearance</label>
15519
+ <select name="setting_appearance" id="setting_appearance" class="${selectClass}" style="color: white; background-color: rgb(39, 39, 42);">
15520
+ <option value="always" ${!settings.appearance || settings.appearance === "always" ? "selected" : ""} style="background-color: rgb(39, 39, 42); color: white;">Always - Pre-clearance enabled (verifies immediately)</option>
15521
+ <option value="execute" ${settings.appearance === "execute" ? "selected" : ""} style="background-color: rgb(39, 39, 42); color: white;">Execute - Challenge on form submit</option>
15522
+ <option value="interaction-only" ${settings.appearance === "interaction-only" ? "selected" : ""} style="background-color: rgb(39, 39, 42); color: white;">Interaction Only - Only after user interaction</option>
15523
+ </select>
15524
+ <p class="text-xs text-gray-400 mt-1">Controls when Turnstile verification occurs. <strong>Always:</strong> Verifies immediately (pre-clearance). <strong>Execute:</strong> Verifies on form submit. <strong>Interaction Only:</strong> Only after user interaction.</p>
15525
+ </div>
15526
+ `;
15527
+ }
13692
15528
  function renderNoSettings(plugin) {
13693
15529
  if (plugin.id === "seed-data" || plugin.name === "seed-data") {
13694
15530
  return `
@@ -13828,7 +15664,7 @@ function formatTimestamp(timestamp) {
13828
15664
 
13829
15665
  // src/routes/admin-plugins.ts
13830
15666
  var adminPluginRoutes = new hono.Hono();
13831
- adminPluginRoutes.use("*", chunk7I5INVNR_cjs.requireAuth());
15667
+ adminPluginRoutes.use("*", chunkT3YIKW2A_cjs.requireAuth());
13832
15668
  var AVAILABLE_PLUGINS = [
13833
15669
  {
13834
15670
  id: "third-party-faq",
@@ -13920,6 +15756,32 @@ var AVAILABLE_PLUGINS = [
13920
15756
  permissions: [],
13921
15757
  dependencies: [],
13922
15758
  is_core: false
15759
+ },
15760
+ {
15761
+ id: "turnstile",
15762
+ name: "turnstile-plugin",
15763
+ display_name: "Cloudflare Turnstile",
15764
+ description: "CAPTCHA-free bot protection for forms using Cloudflare Turnstile. Provides seamless spam prevention with configurable modes, themes, and pre-clearance options.",
15765
+ version: "1.0.0",
15766
+ author: "SonicJS Team",
15767
+ category: "security",
15768
+ icon: "\u{1F6E1}\uFE0F",
15769
+ permissions: [],
15770
+ dependencies: [],
15771
+ is_core: true
15772
+ },
15773
+ {
15774
+ id: "ai-search",
15775
+ name: "ai-search-plugin",
15776
+ display_name: "AI Search",
15777
+ description: "Advanced search with Cloudflare AI Search. Full-text search, semantic search, and advanced filtering across all content collections.",
15778
+ version: "1.0.0",
15779
+ author: "SonicJS Team",
15780
+ category: "search",
15781
+ icon: "\u{1F50D}",
15782
+ permissions: [],
15783
+ dependencies: [],
15784
+ is_core: true
13923
15785
  }
13924
15786
  ];
13925
15787
  adminPluginRoutes.get("/", async (c) => {
@@ -13929,7 +15791,7 @@ adminPluginRoutes.get("/", async (c) => {
13929
15791
  if (user?.role !== "admin") {
13930
15792
  return c.text("Access denied", 403);
13931
15793
  }
13932
- const pluginService = new chunkILZ3DP4I_cjs.PluginService(db);
15794
+ const pluginService = new chunkMPT5PA6U_cjs.PluginService(db);
13933
15795
  let installedPlugins = [];
13934
15796
  let stats = { total: 0, active: 0, inactive: 0, errors: 0, uninstalled: 0 };
13935
15797
  try {
@@ -13998,10 +15860,13 @@ adminPluginRoutes.get("/:id", async (c) => {
13998
15860
  const user = c.get("user");
13999
15861
  const db = c.env.DB;
14000
15862
  const pluginId = c.req.param("id");
15863
+ if (pluginId === "ai-search") {
15864
+ return c.text("", 404);
15865
+ }
14001
15866
  if (user?.role !== "admin") {
14002
15867
  return c.redirect("/admin/plugins");
14003
15868
  }
14004
- const pluginService = new chunkILZ3DP4I_cjs.PluginService(db);
15869
+ const pluginService = new chunkMPT5PA6U_cjs.PluginService(db);
14005
15870
  const plugin = await pluginService.getPlugin(pluginId);
14006
15871
  if (!plugin) {
14007
15872
  return c.text("Plugin not found", 404);
@@ -14055,7 +15920,7 @@ adminPluginRoutes.post("/:id/activate", async (c) => {
14055
15920
  if (user?.role !== "admin") {
14056
15921
  return c.json({ error: "Access denied" }, 403);
14057
15922
  }
14058
- const pluginService = new chunkILZ3DP4I_cjs.PluginService(db);
15923
+ const pluginService = new chunkMPT5PA6U_cjs.PluginService(db);
14059
15924
  await pluginService.activatePlugin(pluginId);
14060
15925
  return c.json({ success: true });
14061
15926
  } catch (error) {
@@ -14072,7 +15937,7 @@ adminPluginRoutes.post("/:id/deactivate", async (c) => {
14072
15937
  if (user?.role !== "admin") {
14073
15938
  return c.json({ error: "Access denied" }, 403);
14074
15939
  }
14075
- const pluginService = new chunkILZ3DP4I_cjs.PluginService(db);
15940
+ const pluginService = new chunkMPT5PA6U_cjs.PluginService(db);
14076
15941
  await pluginService.deactivatePlugin(pluginId);
14077
15942
  return c.json({ success: true });
14078
15943
  } catch (error) {
@@ -14089,7 +15954,7 @@ adminPluginRoutes.post("/install", async (c) => {
14089
15954
  return c.json({ error: "Access denied" }, 403);
14090
15955
  }
14091
15956
  const body = await c.req.json();
14092
- const pluginService = new chunkILZ3DP4I_cjs.PluginService(db);
15957
+ const pluginService = new chunkMPT5PA6U_cjs.PluginService(db);
14093
15958
  if (body.name === "faq-plugin") {
14094
15959
  const faqPlugin = await pluginService.installPlugin({
14095
15960
  id: "third-party-faq",
@@ -14290,6 +16155,60 @@ adminPluginRoutes.post("/install", async (c) => {
14290
16155
  });
14291
16156
  return c.json({ success: true, plugin: easyMdxPlugin2 });
14292
16157
  }
16158
+ if (body.name === "ai-search-plugin" || body.name === "ai-search") {
16159
+ const defaultSettings = {
16160
+ enabled: true,
16161
+ ai_mode_enabled: true,
16162
+ selected_collections: [],
16163
+ dismissed_collections: [],
16164
+ autocomplete_enabled: true,
16165
+ cache_duration: 1,
16166
+ results_limit: 20,
16167
+ index_media: false
16168
+ };
16169
+ const aiSearchPlugin = await pluginService.installPlugin({
16170
+ id: "ai-search",
16171
+ name: "ai-search-plugin",
16172
+ display_name: "AI Search",
16173
+ description: "Advanced search with Cloudflare AI Search. Full-text search, semantic search, and advanced filtering across all content collections.",
16174
+ version: "1.0.0",
16175
+ author: "SonicJS Team",
16176
+ category: "search",
16177
+ icon: "\u{1F50D}",
16178
+ permissions: [],
16179
+ dependencies: [],
16180
+ is_core: true,
16181
+ settings: defaultSettings
16182
+ });
16183
+ return c.json({ success: true, plugin: aiSearchPlugin });
16184
+ }
16185
+ if (body.name === "turnstile-plugin") {
16186
+ const turnstilePlugin = await pluginService.installPlugin({
16187
+ id: "turnstile",
16188
+ name: "turnstile-plugin",
16189
+ display_name: "Cloudflare Turnstile",
16190
+ description: "CAPTCHA-free bot protection for forms using Cloudflare Turnstile. Provides seamless spam prevention with configurable modes, themes, and pre-clearance options.",
16191
+ version: "1.0.0",
16192
+ author: "SonicJS Team",
16193
+ category: "security",
16194
+ icon: "\u{1F6E1}\uFE0F",
16195
+ permissions: [],
16196
+ dependencies: [],
16197
+ is_core: true,
16198
+ settings: {
16199
+ siteKey: "",
16200
+ secretKey: "",
16201
+ theme: "auto",
16202
+ size: "normal",
16203
+ mode: "managed",
16204
+ appearance: "always",
16205
+ preClearanceEnabled: false,
16206
+ preClearanceLevel: "managed",
16207
+ enabled: false
16208
+ }
16209
+ });
16210
+ return c.json({ success: true, plugin: turnstilePlugin });
16211
+ }
14293
16212
  return c.json({ error: "Plugin not found in registry" }, 404);
14294
16213
  } catch (error) {
14295
16214
  console.error("Error installing plugin:", error);
@@ -14305,7 +16224,7 @@ adminPluginRoutes.post("/:id/uninstall", async (c) => {
14305
16224
  if (user?.role !== "admin") {
14306
16225
  return c.json({ error: "Access denied" }, 403);
14307
16226
  }
14308
- const pluginService = new chunkILZ3DP4I_cjs.PluginService(db);
16227
+ const pluginService = new chunkMPT5PA6U_cjs.PluginService(db);
14309
16228
  await pluginService.uninstallPlugin(pluginId);
14310
16229
  return c.json({ success: true });
14311
16230
  } catch (error) {
@@ -14323,7 +16242,7 @@ adminPluginRoutes.post("/:id/settings", async (c) => {
14323
16242
  return c.json({ error: "Access denied" }, 403);
14324
16243
  }
14325
16244
  const settings = await c.req.json();
14326
- const pluginService = new chunkILZ3DP4I_cjs.PluginService(db);
16245
+ const pluginService = new chunkMPT5PA6U_cjs.PluginService(db);
14327
16246
  await pluginService.updatePluginSettings(pluginId, settings);
14328
16247
  return c.json({ success: true });
14329
16248
  } catch (error) {
@@ -14344,7 +16263,7 @@ function formatLastUpdated(timestamp) {
14344
16263
  }
14345
16264
 
14346
16265
  // src/templates/pages/admin-logs-list.template.ts
14347
- chunkAZLU3ROK_cjs.init_admin_layout_catalyst_template();
16266
+ chunkBZC4FYW7_cjs.init_admin_layout_catalyst_template();
14348
16267
  function renderLogsListPage(data) {
14349
16268
  const { logs, pagination, filters, user } = data;
14350
16269
  const content = `
@@ -14655,7 +16574,7 @@ function renderLogsListPage(data) {
14655
16574
  user,
14656
16575
  content
14657
16576
  };
14658
- return chunkAZLU3ROK_cjs.renderAdminLayoutCatalyst(layoutData);
16577
+ return chunkBZC4FYW7_cjs.renderAdminLayoutCatalyst(layoutData);
14659
16578
  }
14660
16579
  function renderLogDetailsPage(data) {
14661
16580
  const { log, user } = data;
@@ -14867,7 +16786,7 @@ function renderLogDetailsPage(data) {
14867
16786
  </div>
14868
16787
  </div>
14869
16788
  `;
14870
- return chunkAZLU3ROK_cjs.adminLayoutV2({
16789
+ return chunkBZC4FYW7_cjs.adminLayoutV2({
14871
16790
  title: `Log Details - ${log.id}`,
14872
16791
  user,
14873
16792
  content
@@ -15110,7 +17029,7 @@ function renderLogConfigPage(data) {
15110
17029
 
15111
17030
  <script src="https://unpkg.com/htmx.org@1.9.6"></script>
15112
17031
  `;
15113
- return chunkAZLU3ROK_cjs.adminLayoutV2({
17032
+ return chunkBZC4FYW7_cjs.adminLayoutV2({
15114
17033
  title: "Log Configuration",
15115
17034
  user,
15116
17035
  content
@@ -15119,7 +17038,7 @@ function renderLogConfigPage(data) {
15119
17038
 
15120
17039
  // src/routes/admin-logs.ts
15121
17040
  var adminLogsRoutes = new hono.Hono();
15122
- adminLogsRoutes.use("*", chunk7I5INVNR_cjs.requireAuth());
17041
+ adminLogsRoutes.use("*", chunkT3YIKW2A_cjs.requireAuth());
15123
17042
  adminLogsRoutes.get("/", async (c) => {
15124
17043
  try {
15125
17044
  const user = c.get("user");
@@ -15491,7 +17410,7 @@ adminDesignRoutes.get("/", (c) => {
15491
17410
  role: user.role
15492
17411
  } : void 0
15493
17412
  };
15494
- return c.html(chunkAZLU3ROK_cjs.renderDesignPage(pageData));
17413
+ return c.html(chunkBZC4FYW7_cjs.renderDesignPage(pageData));
15495
17414
  });
15496
17415
  var adminCheckboxRoutes = new hono.Hono();
15497
17416
  adminCheckboxRoutes.get("/", (c) => {
@@ -15503,7 +17422,7 @@ adminCheckboxRoutes.get("/", (c) => {
15503
17422
  role: user.role
15504
17423
  } : void 0
15505
17424
  };
15506
- return c.html(chunkAZLU3ROK_cjs.renderCheckboxPage(pageData));
17425
+ return c.html(chunkBZC4FYW7_cjs.renderCheckboxPage(pageData));
15507
17426
  });
15508
17427
 
15509
17428
  // src/templates/pages/admin-testimonials-form.template.ts
@@ -15531,7 +17450,7 @@ function renderTestimonialsForm(data) {
15531
17450
  </div>
15532
17451
  </div>
15533
17452
 
15534
- ${message ? chunkAZLU3ROK_cjs.renderAlert({ type: messageType || "info", message, dismissible: true }) : ""}
17453
+ ${message ? chunkBZC4FYW7_cjs.renderAlert({ type: messageType || "info", message, dismissible: true }) : ""}
15535
17454
 
15536
17455
  <!-- Form -->
15537
17456
  <div class="backdrop-blur-xl bg-white/10 rounded-xl border border-white/20 shadow-2xl">
@@ -15760,7 +17679,7 @@ function renderTestimonialsForm(data) {
15760
17679
  user: data.user,
15761
17680
  content: pageContent
15762
17681
  };
15763
- return chunkAZLU3ROK_cjs.renderAdminLayout(layoutData);
17682
+ return chunkBZC4FYW7_cjs.renderAdminLayout(layoutData);
15764
17683
  }
15765
17684
  function escapeHtml4(unsafe) {
15766
17685
  return unsafe.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
@@ -15786,7 +17705,7 @@ adminTestimonialsRoutes.get("/", async (c) => {
15786
17705
  const offset = (currentPage - 1) * limit;
15787
17706
  const db = c.env?.DB;
15788
17707
  if (!db) {
15789
- return c.html(chunkAZLU3ROK_cjs.renderTestimonialsList({
17708
+ return c.html(chunkBZC4FYW7_cjs.renderTestimonialsList({
15790
17709
  testimonials: [],
15791
17710
  totalCount: 0,
15792
17711
  currentPage: 1,
@@ -15826,7 +17745,7 @@ adminTestimonialsRoutes.get("/", async (c) => {
15826
17745
  `;
15827
17746
  const { results: testimonials } = await db.prepare(dataQuery).bind(...params, limit, offset).all();
15828
17747
  const totalPages = Math.ceil(totalCount / limit);
15829
- return c.html(chunkAZLU3ROK_cjs.renderTestimonialsList({
17748
+ return c.html(chunkBZC4FYW7_cjs.renderTestimonialsList({
15830
17749
  testimonials: testimonials || [],
15831
17750
  totalCount,
15832
17751
  currentPage,
@@ -15840,7 +17759,7 @@ adminTestimonialsRoutes.get("/", async (c) => {
15840
17759
  } catch (error) {
15841
17760
  console.error("Error fetching testimonials:", error);
15842
17761
  const user = c.get("user");
15843
- return c.html(chunkAZLU3ROK_cjs.renderTestimonialsList({
17762
+ return c.html(chunkBZC4FYW7_cjs.renderTestimonialsList({
15844
17763
  testimonials: [],
15845
17764
  totalCount: 0,
15846
17765
  currentPage: 1,
@@ -16159,7 +18078,7 @@ function renderCodeExamplesForm(data) {
16159
18078
  </div>
16160
18079
  </div>
16161
18080
 
16162
- ${message ? chunkAZLU3ROK_cjs.renderAlert({ type: messageType || "info", message, dismissible: true }) : ""}
18081
+ ${message ? chunkBZC4FYW7_cjs.renderAlert({ type: messageType || "info", message, dismissible: true }) : ""}
16163
18082
 
16164
18083
  <!-- Form -->
16165
18084
  <div class="backdrop-blur-xl bg-white/10 rounded-xl border border-white/20 shadow-2xl">
@@ -16429,7 +18348,7 @@ function renderCodeExamplesForm(data) {
16429
18348
  user: data.user,
16430
18349
  content: pageContent
16431
18350
  };
16432
- return chunkAZLU3ROK_cjs.renderAdminLayout(layoutData);
18351
+ return chunkBZC4FYW7_cjs.renderAdminLayout(layoutData);
16433
18352
  }
16434
18353
  function escapeHtml5(unsafe) {
16435
18354
  return unsafe.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
@@ -16456,7 +18375,7 @@ adminCodeExamplesRoutes.get("/", async (c) => {
16456
18375
  const offset = (currentPage - 1) * limit;
16457
18376
  const db = c.env?.DB;
16458
18377
  if (!db) {
16459
- return c.html(chunkAZLU3ROK_cjs.renderCodeExamplesList({
18378
+ return c.html(chunkBZC4FYW7_cjs.renderCodeExamplesList({
16460
18379
  codeExamples: [],
16461
18380
  totalCount: 0,
16462
18381
  currentPage: 1,
@@ -16496,7 +18415,7 @@ adminCodeExamplesRoutes.get("/", async (c) => {
16496
18415
  `;
16497
18416
  const { results: codeExamples } = await db.prepare(dataQuery).bind(...params, limit, offset).all();
16498
18417
  const totalPages = Math.ceil(totalCount / limit);
16499
- return c.html(chunkAZLU3ROK_cjs.renderCodeExamplesList({
18418
+ return c.html(chunkBZC4FYW7_cjs.renderCodeExamplesList({
16500
18419
  codeExamples: codeExamples || [],
16501
18420
  totalCount,
16502
18421
  currentPage,
@@ -16510,7 +18429,7 @@ adminCodeExamplesRoutes.get("/", async (c) => {
16510
18429
  } catch (error) {
16511
18430
  console.error("Error fetching code examples:", error);
16512
18431
  const user = c.get("user");
16513
- return c.html(chunkAZLU3ROK_cjs.renderCodeExamplesList({
18432
+ return c.html(chunkBZC4FYW7_cjs.renderCodeExamplesList({
16514
18433
  codeExamples: [],
16515
18434
  totalCount: 0,
16516
18435
  currentPage: 1,
@@ -16899,7 +18818,7 @@ function renderDashboardPage(data) {
16899
18818
  version: data.version,
16900
18819
  content: pageContent
16901
18820
  };
16902
- return chunkAZLU3ROK_cjs.renderAdminLayout(layoutData);
18821
+ return chunkBZC4FYW7_cjs.renderAdminLayout(layoutData);
16903
18822
  }
16904
18823
  function renderStatsCards(stats) {
16905
18824
  const cards = [
@@ -17447,9 +19366,9 @@ function renderStorageUsage(databaseSizeBytes, mediaSizeBytes) {
17447
19366
  }
17448
19367
 
17449
19368
  // src/routes/admin-dashboard.ts
17450
- var VERSION = chunkFYEDK7K7_cjs.getCoreVersion();
19369
+ var VERSION = chunkYMTTGHEK_cjs.getCoreVersion();
17451
19370
  var router = new hono.Hono();
17452
- router.use("*", chunk7I5INVNR_cjs.requireAuth());
19371
+ router.use("*", chunkT3YIKW2A_cjs.requireAuth());
17453
19372
  router.get("/", async (c) => {
17454
19373
  const user = c.get("user");
17455
19374
  try {
@@ -17674,7 +19593,7 @@ router.get("/system-status", async (c) => {
17674
19593
  });
17675
19594
 
17676
19595
  // src/templates/pages/admin-collections-list.template.ts
17677
- chunkAZLU3ROK_cjs.init_admin_layout_catalyst_template();
19596
+ chunkBZC4FYW7_cjs.init_admin_layout_catalyst_template();
17678
19597
 
17679
19598
  // src/templates/components/table.template.ts
17680
19599
  function renderTable2(data) {
@@ -18148,11 +20067,11 @@ function renderCollectionsListPage(data) {
18148
20067
  version: data.version,
18149
20068
  content: pageContent
18150
20069
  };
18151
- return chunkAZLU3ROK_cjs.renderAdminLayoutCatalyst(layoutData);
20070
+ return chunkBZC4FYW7_cjs.renderAdminLayoutCatalyst(layoutData);
18152
20071
  }
18153
20072
 
18154
20073
  // src/templates/pages/admin-collections-form.template.ts
18155
- chunkAZLU3ROK_cjs.init_admin_layout_catalyst_template();
20074
+ chunkBZC4FYW7_cjs.init_admin_layout_catalyst_template();
18156
20075
  function getFieldTypeBadge(fieldType) {
18157
20076
  const typeLabels = {
18158
20077
  "text": "Text",
@@ -18413,7 +20332,7 @@ function renderCollectionFormPage(data) {
18413
20332
  }
18414
20333
  </style>
18415
20334
 
18416
- ${chunkAZLU3ROK_cjs.renderForm(formData)}
20335
+ ${chunkBZC4FYW7_cjs.renderForm(formData)}
18417
20336
 
18418
20337
  ${isEdit && data.managed ? `
18419
20338
  <!-- Read-Only Fields Display for Managed Collections -->
@@ -19202,12 +21121,12 @@ function renderCollectionFormPage(data) {
19202
21121
  version: data.version,
19203
21122
  content: pageContent
19204
21123
  };
19205
- return chunkAZLU3ROK_cjs.renderAdminLayoutCatalyst(layoutData);
21124
+ return chunkBZC4FYW7_cjs.renderAdminLayoutCatalyst(layoutData);
19206
21125
  }
19207
21126
 
19208
21127
  // src/routes/admin-collections.ts
19209
21128
  var adminCollectionsRoutes = new hono.Hono();
19210
- adminCollectionsRoutes.use("*", chunk7I5INVNR_cjs.requireAuth());
21129
+ adminCollectionsRoutes.use("*", chunkT3YIKW2A_cjs.requireAuth());
19211
21130
  adminCollectionsRoutes.get("/", async (c) => {
19212
21131
  try {
19213
21132
  const user = c.get("user");
@@ -19463,16 +21382,30 @@ adminCollectionsRoutes.get("/:id", async (c) => {
19463
21382
  const schema = typeof collection.schema === "string" ? JSON.parse(collection.schema) : collection.schema;
19464
21383
  if (schema && schema.properties) {
19465
21384
  let fieldOrder = 0;
19466
- fields = Object.entries(schema.properties).map(([fieldName, fieldConfig]) => ({
19467
- id: `schema-${fieldName}`,
19468
- field_name: fieldName,
19469
- field_type: fieldConfig.type || "string",
19470
- field_label: fieldConfig.title || fieldName,
19471
- field_options: fieldConfig,
19472
- field_order: fieldOrder++,
19473
- is_required: fieldConfig.required === true || schema.required && schema.required.includes(fieldName),
19474
- is_searchable: fieldConfig.searchable === true || false
19475
- }));
21385
+ fields = Object.entries(schema.properties).map(([fieldName, fieldConfig]) => {
21386
+ let fieldType = fieldConfig.type || "string";
21387
+ if (fieldConfig.enum) {
21388
+ fieldType = "select";
21389
+ } else if (fieldConfig.format === "richtext") {
21390
+ fieldType = "richtext";
21391
+ } else if (fieldConfig.format === "media") {
21392
+ fieldType = "media";
21393
+ } else if (fieldConfig.format === "date-time") {
21394
+ fieldType = "date";
21395
+ } else if (fieldConfig.type === "slug" || fieldConfig.format === "slug") {
21396
+ fieldType = "slug";
21397
+ }
21398
+ return {
21399
+ id: `schema-${fieldName}`,
21400
+ field_name: fieldName,
21401
+ field_type: fieldType,
21402
+ field_label: fieldConfig.title || fieldName,
21403
+ field_options: fieldConfig,
21404
+ field_order: fieldOrder++,
21405
+ is_required: fieldConfig.required === true || schema.required && schema.required.includes(fieldName),
21406
+ is_searchable: fieldConfig.searchable === true || false
21407
+ };
21408
+ });
19476
21409
  }
19477
21410
  } catch (e) {
19478
21411
  console.error("Error parsing collection schema:", e);
@@ -19687,6 +21620,9 @@ adminCollectionsRoutes.post("/:id/fields", async (c) => {
19687
21620
  fieldConfig.enum = parsedOptions.options || [];
19688
21621
  } else if (fieldType === "media") {
19689
21622
  fieldConfig.format = "media";
21623
+ } else if (fieldType === "slug") {
21624
+ fieldConfig.type = "slug";
21625
+ fieldConfig.format = "slug";
19690
21626
  } else if (fieldType === "quill") {
19691
21627
  fieldConfig.type = "quill";
19692
21628
  } else if (fieldType === "mdxeditor") {
@@ -19906,7 +21842,7 @@ adminCollectionsRoutes.post("/:collectionId/fields/reorder", async (c) => {
19906
21842
  });
19907
21843
 
19908
21844
  // src/templates/pages/admin-settings.template.ts
19909
- chunkAZLU3ROK_cjs.init_admin_layout_catalyst_template();
21845
+ chunkBZC4FYW7_cjs.init_admin_layout_catalyst_template();
19910
21846
  function renderSettingsPage(data) {
19911
21847
  const activeTab = data.activeTab || "general";
19912
21848
  const pageContent = `
@@ -20288,7 +22224,7 @@ function renderSettingsPage(data) {
20288
22224
  version: data.version,
20289
22225
  content: pageContent
20290
22226
  };
20291
- return chunkAZLU3ROK_cjs.renderAdminLayoutCatalyst(layoutData);
22227
+ return chunkBZC4FYW7_cjs.renderAdminLayoutCatalyst(layoutData);
20292
22228
  }
20293
22229
  function renderTabButton(tabId, label, iconPath, activeTab) {
20294
22230
  const isActive = activeTab === tabId;
@@ -21370,7 +23306,7 @@ function renderDatabaseToolsSettings(settings) {
21370
23306
 
21371
23307
  // src/routes/admin-settings.ts
21372
23308
  var adminSettingsRoutes = new hono.Hono();
21373
- adminSettingsRoutes.use("*", chunk7I5INVNR_cjs.requireAuth());
23309
+ adminSettingsRoutes.use("*", chunkT3YIKW2A_cjs.requireAuth());
21374
23310
  function getMockSettings(user) {
21375
23311
  return {
21376
23312
  general: {
@@ -21538,7 +23474,7 @@ adminSettingsRoutes.get("/database-tools", (c) => {
21538
23474
  adminSettingsRoutes.get("/api/migrations/status", async (c) => {
21539
23475
  try {
21540
23476
  const db = c.env.DB;
21541
- const migrationService = new chunk2MI3LZFH_cjs.MigrationService(db);
23477
+ const migrationService = new chunkIIRVZSP2_cjs.MigrationService(db);
21542
23478
  const status = await migrationService.getMigrationStatus();
21543
23479
  return c.json({
21544
23480
  success: true,
@@ -21562,7 +23498,7 @@ adminSettingsRoutes.post("/api/migrations/run", async (c) => {
21562
23498
  }, 403);
21563
23499
  }
21564
23500
  const db = c.env.DB;
21565
- const migrationService = new chunk2MI3LZFH_cjs.MigrationService(db);
23501
+ const migrationService = new chunkIIRVZSP2_cjs.MigrationService(db);
21566
23502
  const result = await migrationService.runPendingMigrations();
21567
23503
  return c.json({
21568
23504
  success: result.success,
@@ -21580,7 +23516,7 @@ adminSettingsRoutes.post("/api/migrations/run", async (c) => {
21580
23516
  adminSettingsRoutes.get("/api/migrations/validate", async (c) => {
21581
23517
  try {
21582
23518
  const db = c.env.DB;
21583
- const migrationService = new chunk2MI3LZFH_cjs.MigrationService(db);
23519
+ const migrationService = new chunkIIRVZSP2_cjs.MigrationService(db);
21584
23520
  const validation = await migrationService.validateSchema();
21585
23521
  return c.json({
21586
23522
  success: true,
@@ -21804,7 +23740,6 @@ var ROUTES_INFO = {
21804
23740
  reference: "https://github.com/sonicjs/sonicjs"
21805
23741
  };
21806
23742
 
21807
- exports.PluginBuilder = PluginBuilder;
21808
23743
  exports.ROUTES_INFO = ROUTES_INFO;
21809
23744
  exports.adminCheckboxRoutes = adminCheckboxRoutes;
21810
23745
  exports.adminCollectionsRoutes = adminCollectionsRoutes;
@@ -21822,9 +23757,8 @@ exports.api_default = api_default;
21822
23757
  exports.api_media_default = api_media_default;
21823
23758
  exports.api_system_default = api_system_default;
21824
23759
  exports.auth_default = auth_default;
21825
- exports.checkAdminUserExists = checkAdminUserExists;
21826
23760
  exports.router = router;
21827
23761
  exports.test_cleanup_default = test_cleanup_default;
21828
23762
  exports.userRoutes = userRoutes;
21829
- //# sourceMappingURL=chunk-A4SVOGG6.cjs.map
21830
- //# sourceMappingURL=chunk-A4SVOGG6.cjs.map
23763
+ //# sourceMappingURL=chunk-N7TDLOUE.cjs.map
23764
+ //# sourceMappingURL=chunk-N7TDLOUE.cjs.map