@sonicjs-cms/core 2.9.0 → 2.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/dist/{chunk-DQZVU3WB.cjs → chunk-5GO3AMON.cjs} +13 -7
  2. package/dist/chunk-5GO3AMON.cjs.map +1 -0
  3. package/dist/{chunk-YFJJU26H.js → chunk-BUPNX3ZM.js} +375 -3
  4. package/dist/chunk-BUPNX3ZM.js.map +1 -0
  5. package/dist/{chunk-SHU7Q66Q.cjs → chunk-E2GKK5HX.cjs} +7 -3
  6. package/dist/chunk-E2GKK5HX.cjs.map +1 -0
  7. package/dist/{chunk-LDFMYRG6.cjs → chunk-EAJJHE5F.cjs} +9 -2
  8. package/dist/chunk-EAJJHE5F.cjs.map +1 -0
  9. package/dist/{chunk-STTZVLY2.js → chunk-FW5CGNM2.js} +9 -2
  10. package/dist/chunk-FW5CGNM2.js.map +1 -0
  11. package/dist/{chunk-KSB6FXOP.cjs → chunk-HGKBMUYY.cjs} +1194 -278
  12. package/dist/chunk-HGKBMUYY.cjs.map +1 -0
  13. package/dist/{chunk-25YNV4RK.js → chunk-JFMBYQTC.js} +10 -4
  14. package/dist/chunk-JFMBYQTC.js.map +1 -0
  15. package/dist/{chunk-64APW3DW.cjs → chunk-LFAQUR7P.cjs} +9 -2
  16. package/dist/chunk-LFAQUR7P.cjs.map +1 -0
  17. package/dist/{chunk-2JGQKF7B.js → chunk-SDAGUFOF.js} +1079 -163
  18. package/dist/chunk-SDAGUFOF.js.map +1 -0
  19. package/dist/{chunk-MPT5PA6U.cjs → chunk-TWCQVJ6M.cjs} +381 -2
  20. package/dist/chunk-TWCQVJ6M.cjs.map +1 -0
  21. package/dist/{chunk-7JMMLHPQ.js → chunk-VJCLJH3X.js} +9 -2
  22. package/dist/chunk-VJCLJH3X.js.map +1 -0
  23. package/dist/{chunk-3FHMXGLF.js → chunk-YXTFJPMN.js} +7 -3
  24. package/dist/chunk-YXTFJPMN.js.map +1 -0
  25. package/dist/{collection-config-DckWhkdL.d.cts → collection-config-B4PG-AaF.d.cts} +2 -0
  26. package/dist/{collection-config-DckWhkdL.d.ts → collection-config-B4PG-AaF.d.ts} +2 -0
  27. package/dist/index.cjs +170 -142
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.d.cts +3 -3
  30. package/dist/index.d.ts +3 -3
  31. package/dist/index.js +10 -10
  32. package/dist/index.js.map +1 -1
  33. package/dist/middleware.cjs +29 -29
  34. package/dist/middleware.js +3 -3
  35. package/dist/migrations-ADK6YNM2.js +4 -0
  36. package/dist/{migrations-SZSR3C3G.js.map → migrations-ADK6YNM2.js.map} +1 -1
  37. package/dist/migrations-EM2D6EG2.cjs +13 -0
  38. package/dist/{migrations-QQWGDWGB.cjs.map → migrations-EM2D6EG2.cjs.map} +1 -1
  39. package/dist/{plugin-bootstrap-BAz7NY0H.d.cts → plugin-bootstrap-B8PXeGj_.d.cts} +230 -2
  40. package/dist/{plugin-bootstrap-Cz3-bj8X.d.ts → plugin-bootstrap-CD63DZ-p.d.ts} +230 -2
  41. package/dist/routes.cjs +29 -29
  42. package/dist/routes.js +6 -6
  43. package/dist/services.cjs +60 -32
  44. package/dist/services.d.cts +2 -2
  45. package/dist/services.d.ts +2 -2
  46. package/dist/services.js +3 -3
  47. package/dist/types.d.cts +1 -1
  48. package/dist/types.d.ts +1 -1
  49. package/dist/utils.cjs +11 -11
  50. package/dist/utils.d.cts +1 -1
  51. package/dist/utils.d.ts +1 -1
  52. package/dist/utils.js +1 -1
  53. package/migrations/033_form_content_integration.sql +19 -0
  54. package/package.json +5 -1
  55. package/dist/chunk-25YNV4RK.js.map +0 -1
  56. package/dist/chunk-2JGQKF7B.js.map +0 -1
  57. package/dist/chunk-3FHMXGLF.js.map +0 -1
  58. package/dist/chunk-64APW3DW.cjs.map +0 -1
  59. package/dist/chunk-7JMMLHPQ.js.map +0 -1
  60. package/dist/chunk-DQZVU3WB.cjs.map +0 -1
  61. package/dist/chunk-KSB6FXOP.cjs.map +0 -1
  62. package/dist/chunk-LDFMYRG6.cjs.map +0 -1
  63. package/dist/chunk-MPT5PA6U.cjs.map +0 -1
  64. package/dist/chunk-SHU7Q66Q.cjs.map +0 -1
  65. package/dist/chunk-STTZVLY2.js.map +0 -1
  66. package/dist/chunk-YFJJU26H.js.map +0 -1
  67. package/dist/migrations-QQWGDWGB.cjs +0 -13
  68. package/dist/migrations-SZSR3C3G.js +0 -4
@@ -1,12 +1,12 @@
1
1
  'use strict';
2
2
 
3
- var chunk64APW3DW_cjs = require('./chunk-64APW3DW.cjs');
4
- var chunkDQZVU3WB_cjs = require('./chunk-DQZVU3WB.cjs');
5
- var chunkMPT5PA6U_cjs = require('./chunk-MPT5PA6U.cjs');
6
- var chunkLDFMYRG6_cjs = require('./chunk-LDFMYRG6.cjs');
3
+ var chunkLFAQUR7P_cjs = require('./chunk-LFAQUR7P.cjs');
4
+ var chunk5GO3AMON_cjs = require('./chunk-5GO3AMON.cjs');
5
+ var chunkTWCQVJ6M_cjs = require('./chunk-TWCQVJ6M.cjs');
6
+ var chunkEAJJHE5F_cjs = require('./chunk-EAJJHE5F.cjs');
7
7
  var chunkLTKV7AE5_cjs = require('./chunk-LTKV7AE5.cjs');
8
8
  var chunk6FHNRRJ3_cjs = require('./chunk-6FHNRRJ3.cjs');
9
- var chunkSHU7Q66Q_cjs = require('./chunk-SHU7Q66Q.cjs');
9
+ var chunkE2GKK5HX_cjs = require('./chunk-E2GKK5HX.cjs');
10
10
  var chunkRCQ2HIQD_cjs = require('./chunk-RCQ2HIQD.cjs');
11
11
  var chunkMNWKYY5E_cjs = require('./chunk-MNWKYY5E.cjs');
12
12
  var hono = require('hono');
@@ -121,7 +121,7 @@ apiContentCrudRoutes.get("/:id", async (c) => {
121
121
  }, 500);
122
122
  }
123
123
  });
124
- apiContentCrudRoutes.post("/", chunkDQZVU3WB_cjs.requireAuth(), async (c) => {
124
+ apiContentCrudRoutes.post("/", chunk5GO3AMON_cjs.requireAuth(), chunk5GO3AMON_cjs.requireRole(["admin", "editor", "author"]), async (c) => {
125
125
  try {
126
126
  const db = c.env.DB;
127
127
  const user = c.get("user");
@@ -162,7 +162,7 @@ apiContentCrudRoutes.post("/", chunkDQZVU3WB_cjs.requireAuth(), async (c) => {
162
162
  now,
163
163
  now
164
164
  ).run();
165
- const cache = chunk64APW3DW_cjs.getCacheService(chunk64APW3DW_cjs.CACHE_CONFIGS.api);
165
+ const cache = chunkLFAQUR7P_cjs.getCacheService(chunkLFAQUR7P_cjs.CACHE_CONFIGS.api);
166
166
  await cache.invalidate(`content:list:${collectionId}:*`);
167
167
  await cache.invalidate("content-filtered:*");
168
168
  const getStmt = db.prepare("SELECT * FROM content WHERE id = ?");
@@ -187,7 +187,7 @@ apiContentCrudRoutes.post("/", chunkDQZVU3WB_cjs.requireAuth(), async (c) => {
187
187
  }, 500);
188
188
  }
189
189
  });
190
- apiContentCrudRoutes.put("/:id", chunkDQZVU3WB_cjs.requireAuth(), async (c) => {
190
+ apiContentCrudRoutes.put("/:id", chunk5GO3AMON_cjs.requireAuth(), chunk5GO3AMON_cjs.requireRole(["admin", "editor", "author"]), async (c) => {
191
191
  try {
192
192
  const id = c.req.param("id");
193
193
  const db = c.env.DB;
@@ -225,7 +225,7 @@ apiContentCrudRoutes.put("/:id", chunkDQZVU3WB_cjs.requireAuth(), async (c) => {
225
225
  WHERE id = ?
226
226
  `);
227
227
  await updateStmt.bind(...params).run();
228
- const cache = chunk64APW3DW_cjs.getCacheService(chunk64APW3DW_cjs.CACHE_CONFIGS.api);
228
+ const cache = chunkLFAQUR7P_cjs.getCacheService(chunkLFAQUR7P_cjs.CACHE_CONFIGS.api);
229
229
  await cache.delete(cache.generateKey("content", id));
230
230
  await cache.invalidate(`content:list:${existing.collection_id}:*`);
231
231
  await cache.invalidate("content-filtered:*");
@@ -251,7 +251,7 @@ apiContentCrudRoutes.put("/:id", chunkDQZVU3WB_cjs.requireAuth(), async (c) => {
251
251
  }, 500);
252
252
  }
253
253
  });
254
- apiContentCrudRoutes.delete("/:id", chunkDQZVU3WB_cjs.requireAuth(), async (c) => {
254
+ apiContentCrudRoutes.delete("/:id", chunk5GO3AMON_cjs.requireAuth(), chunk5GO3AMON_cjs.requireRole(["admin", "editor", "author"]), async (c) => {
255
255
  try {
256
256
  const id = c.req.param("id");
257
257
  const db = c.env.DB;
@@ -262,7 +262,7 @@ apiContentCrudRoutes.delete("/:id", chunkDQZVU3WB_cjs.requireAuth(), async (c) =
262
262
  }
263
263
  const deleteStmt = db.prepare("DELETE FROM content WHERE id = ?");
264
264
  await deleteStmt.bind(id).run();
265
- const cache = chunk64APW3DW_cjs.getCacheService(chunk64APW3DW_cjs.CACHE_CONFIGS.api);
265
+ const cache = chunkLFAQUR7P_cjs.getCacheService(chunkLFAQUR7P_cjs.CACHE_CONFIGS.api);
266
266
  await cache.delete(cache.generateKey("content", id));
267
267
  await cache.invalidate(`content:list:${existing.collection_id}:*`);
268
268
  await cache.invalidate("content-filtered:*");
@@ -287,7 +287,7 @@ apiRoutes.use("*", async (c, next) => {
287
287
  c.header("X-Response-Time", `${totalTime}ms`);
288
288
  });
289
289
  apiRoutes.use("*", async (c, next) => {
290
- const cacheEnabled = await chunkDQZVU3WB_cjs.isPluginActive(c.env.DB, "core-cache");
290
+ const cacheEnabled = await chunk5GO3AMON_cjs.isPluginActive(c.env.DB, "core-cache");
291
291
  c.set("cacheEnabled", cacheEnabled);
292
292
  await next();
293
293
  });
@@ -724,7 +724,7 @@ apiRoutes.get("/collections", async (c) => {
724
724
  try {
725
725
  const db = c.env.DB;
726
726
  const cacheEnabled = c.get("cacheEnabled");
727
- const cache = chunk64APW3DW_cjs.getCacheService(chunk64APW3DW_cjs.CACHE_CONFIGS.api);
727
+ const cache = chunkLFAQUR7P_cjs.getCacheService(chunkLFAQUR7P_cjs.CACHE_CONFIGS.api);
728
728
  const cacheKey = cache.generateKey("collections", "all");
729
729
  if (cacheEnabled) {
730
730
  const cacheResult = await cache.getWithSource(cacheKey);
@@ -750,7 +750,7 @@ apiRoutes.get("/collections", async (c) => {
750
750
  }
751
751
  c.header("X-Cache-Status", "MISS");
752
752
  c.header("X-Cache-Source", "database");
753
- const stmt = db.prepare("SELECT * FROM collections WHERE is_active = 1");
753
+ const stmt = db.prepare("SELECT * FROM collections WHERE is_active = 1 AND (source_type IS NULL OR source_type = 'user')");
754
754
  const { results } = await stmt.all();
755
755
  const transformedResults = results.map((row) => ({
756
756
  ...row,
@@ -778,7 +778,7 @@ apiRoutes.get("/collections", async (c) => {
778
778
  return c.json({ error: "Failed to fetch collections" }, 500);
779
779
  }
780
780
  });
781
- apiRoutes.get("/content", chunkDQZVU3WB_cjs.optionalAuth(), async (c) => {
781
+ apiRoutes.get("/content", chunk5GO3AMON_cjs.optionalAuth(), async (c) => {
782
782
  const executionStart = Date.now();
783
783
  try {
784
784
  const db = c.env.DB;
@@ -801,13 +801,13 @@ apiRoutes.get("/content", chunkDQZVU3WB_cjs.optionalAuth(), async (c) => {
801
801
  });
802
802
  }
803
803
  }
804
- const filter = chunkSHU7Q66Q_cjs.QueryFilterBuilder.parseFromQuery(queryParams);
804
+ const filter = chunkE2GKK5HX_cjs.QueryFilterBuilder.parseFromQuery(queryParams);
805
805
  const normalizedFilter = normalizePublicContentFilter(filter, c.get("user")?.role);
806
806
  if (!normalizedFilter.limit) {
807
807
  normalizedFilter.limit = 50;
808
808
  }
809
809
  normalizedFilter.limit = Math.min(normalizedFilter.limit, 1e3);
810
- const builder3 = new chunkSHU7Q66Q_cjs.QueryFilterBuilder();
810
+ const builder3 = new chunkE2GKK5HX_cjs.QueryFilterBuilder();
811
811
  const queryResult = builder3.build("content", normalizedFilter);
812
812
  if (queryResult.errors.length > 0) {
813
813
  return c.json({
@@ -816,7 +816,7 @@ apiRoutes.get("/content", chunkDQZVU3WB_cjs.optionalAuth(), async (c) => {
816
816
  }, 400);
817
817
  }
818
818
  const cacheEnabled = c.get("cacheEnabled");
819
- const cache = chunk64APW3DW_cjs.getCacheService(chunk64APW3DW_cjs.CACHE_CONFIGS.api);
819
+ const cache = chunkLFAQUR7P_cjs.getCacheService(chunkLFAQUR7P_cjs.CACHE_CONFIGS.api);
820
820
  const cacheKey = cache.generateKey("content-filtered", JSON.stringify({ filter: normalizedFilter, query: queryResult.sql }));
821
821
  if (cacheEnabled) {
822
822
  const cacheResult = await cache.getWithSource(cacheKey);
@@ -879,7 +879,7 @@ apiRoutes.get("/content", chunkDQZVU3WB_cjs.optionalAuth(), async (c) => {
879
879
  }, 500);
880
880
  }
881
881
  });
882
- apiRoutes.get("/collections/:collection/content", chunkDQZVU3WB_cjs.optionalAuth(), async (c) => {
882
+ apiRoutes.get("/collections/:collection/content", chunk5GO3AMON_cjs.optionalAuth(), async (c) => {
883
883
  const executionStart = Date.now();
884
884
  try {
885
885
  const collection = c.req.param("collection");
@@ -890,7 +890,7 @@ apiRoutes.get("/collections/:collection/content", chunkDQZVU3WB_cjs.optionalAuth
890
890
  if (!collectionResult) {
891
891
  return c.json({ error: "Collection not found" }, 404);
892
892
  }
893
- const filter = chunkSHU7Q66Q_cjs.QueryFilterBuilder.parseFromQuery(queryParams);
893
+ const filter = chunkE2GKK5HX_cjs.QueryFilterBuilder.parseFromQuery(queryParams);
894
894
  const normalizedFilter = normalizePublicContentFilter(filter, c.get("user")?.role);
895
895
  if (!normalizedFilter.where) {
896
896
  normalizedFilter.where = { and: [] };
@@ -907,7 +907,7 @@ apiRoutes.get("/collections/:collection/content", chunkDQZVU3WB_cjs.optionalAuth
907
907
  normalizedFilter.limit = 50;
908
908
  }
909
909
  normalizedFilter.limit = Math.min(normalizedFilter.limit, 1e3);
910
- const builder3 = new chunkSHU7Q66Q_cjs.QueryFilterBuilder();
910
+ const builder3 = new chunkE2GKK5HX_cjs.QueryFilterBuilder();
911
911
  const queryResult = builder3.build("content", normalizedFilter);
912
912
  if (queryResult.errors.length > 0) {
913
913
  return c.json({
@@ -916,7 +916,7 @@ apiRoutes.get("/collections/:collection/content", chunkDQZVU3WB_cjs.optionalAuth
916
916
  }, 400);
917
917
  }
918
918
  const cacheEnabled = c.get("cacheEnabled");
919
- const cache = chunk64APW3DW_cjs.getCacheService(chunk64APW3DW_cjs.CACHE_CONFIGS.api);
919
+ const cache = chunkLFAQUR7P_cjs.getCacheService(chunkLFAQUR7P_cjs.CACHE_CONFIGS.api);
920
920
  const cacheKey = cache.generateKey("collection-content-filtered", `${collection}:${JSON.stringify({ filter: normalizedFilter, query: queryResult.sql })}`);
921
921
  if (cacheEnabled) {
922
922
  const cacheResult = await cache.getWithSource(cacheKey);
@@ -1028,7 +1028,7 @@ var fileValidationSchema = zod.z.object({
1028
1028
  // 50MB max
1029
1029
  });
1030
1030
  var apiMediaRoutes = new hono.Hono();
1031
- apiMediaRoutes.use("*", chunkDQZVU3WB_cjs.requireAuth());
1031
+ apiMediaRoutes.use("*", chunk5GO3AMON_cjs.requireAuth());
1032
1032
  apiMediaRoutes.post("/upload", async (c) => {
1033
1033
  try {
1034
1034
  const user = c.get("user");
@@ -1772,14 +1772,14 @@ apiSystemRoutes.get("/env", (c) => {
1772
1772
  });
1773
1773
  var api_system_default = apiSystemRoutes;
1774
1774
  var adminApiRoutes = new hono.Hono();
1775
- adminApiRoutes.use("*", chunkDQZVU3WB_cjs.requireAuth());
1776
- adminApiRoutes.use("*", chunkDQZVU3WB_cjs.requireRole(["admin", "editor"]));
1775
+ adminApiRoutes.use("*", chunk5GO3AMON_cjs.requireAuth());
1776
+ adminApiRoutes.use("*", chunk5GO3AMON_cjs.requireRole(["admin", "editor"]));
1777
1777
  adminApiRoutes.get("/stats", async (c) => {
1778
1778
  try {
1779
1779
  const db = c.env.DB;
1780
1780
  let collectionsCount = 0;
1781
1781
  try {
1782
- const collectionsStmt = db.prepare("SELECT COUNT(*) as count FROM collections WHERE is_active = 1");
1782
+ const collectionsStmt = db.prepare("SELECT COUNT(*) as count FROM collections WHERE is_active = 1 AND (source_type IS NULL OR source_type = 'user')");
1783
1783
  const collectionsResult = await collectionsStmt.first();
1784
1784
  collectionsCount = collectionsResult?.count || 0;
1785
1785
  } catch (error) {
@@ -1787,7 +1787,7 @@ adminApiRoutes.get("/stats", async (c) => {
1787
1787
  }
1788
1788
  let contentCount = 0;
1789
1789
  try {
1790
- const contentStmt = db.prepare("SELECT COUNT(*) as count FROM content WHERE deleted_at IS NULL");
1790
+ const contentStmt = db.prepare("SELECT COUNT(*) as count FROM content c JOIN collections col ON c.collection_id = col.id WHERE c.deleted_at IS NULL AND (col.source_type IS NULL OR col.source_type = 'user')");
1791
1791
  const contentResult = await contentStmt.first();
1792
1792
  contentCount = contentResult?.count || 0;
1793
1793
  } catch (error) {
@@ -1929,6 +1929,7 @@ adminApiRoutes.get("/collections", async (c) => {
1929
1929
  SELECT id, name, display_name, description, created_at, updated_at, is_active, managed
1930
1930
  FROM collections
1931
1931
  WHERE ${includeInactive ? "1=1" : "is_active = 1"}
1932
+ AND (source_type IS NULL OR source_type = 'user')
1932
1933
  AND (name LIKE ? OR display_name LIKE ? OR description LIKE ?)
1933
1934
  ORDER BY created_at DESC
1934
1935
  `);
@@ -1939,7 +1940,8 @@ adminApiRoutes.get("/collections", async (c) => {
1939
1940
  stmt = db.prepare(`
1940
1941
  SELECT id, name, display_name, description, created_at, updated_at, is_active, managed
1941
1942
  FROM collections
1942
- ${includeInactive ? "" : "WHERE is_active = 1"}
1943
+ WHERE (source_type IS NULL OR source_type = 'user')
1944
+ ${includeInactive ? "" : "AND is_active = 1"}
1943
1945
  ORDER BY created_at DESC
1944
1946
  `);
1945
1947
  const queryResults = await stmt.all();
@@ -2283,7 +2285,7 @@ adminApiRoutes.delete("/collections/:id", async (c) => {
2283
2285
  });
2284
2286
  adminApiRoutes.get("/migrations/status", async (c) => {
2285
2287
  try {
2286
- const { MigrationService: MigrationService2 } = await import('./migrations-QQWGDWGB.cjs');
2288
+ const { MigrationService: MigrationService2 } = await import('./migrations-EM2D6EG2.cjs');
2287
2289
  const db = c.env.DB;
2288
2290
  const migrationService = new MigrationService2(db);
2289
2291
  const status = await migrationService.getMigrationStatus();
@@ -2308,7 +2310,7 @@ adminApiRoutes.post("/migrations/run", async (c) => {
2308
2310
  error: "Unauthorized. Admin access required."
2309
2311
  }, 403);
2310
2312
  }
2311
- const { MigrationService: MigrationService2 } = await import('./migrations-QQWGDWGB.cjs');
2313
+ const { MigrationService: MigrationService2 } = await import('./migrations-EM2D6EG2.cjs');
2312
2314
  const db = c.env.DB;
2313
2315
  const migrationService = new MigrationService2(db);
2314
2316
  const result = await migrationService.runPendingMigrations();
@@ -2327,7 +2329,7 @@ adminApiRoutes.post("/migrations/run", async (c) => {
2327
2329
  });
2328
2330
  adminApiRoutes.get("/migrations/validate", async (c) => {
2329
2331
  try {
2330
- const { MigrationService: MigrationService2 } = await import('./migrations-QQWGDWGB.cjs');
2332
+ const { MigrationService: MigrationService2 } = await import('./migrations-EM2D6EG2.cjs');
2331
2333
  const db = c.env.DB;
2332
2334
  const migrationService = new MigrationService2(db);
2333
2335
  const validation = await migrationService.validateSchema();
@@ -2738,7 +2740,7 @@ var JWT_SECRET_FALLBACK = "your-super-secret-jwt-key-change-in-production";
2738
2740
  async function setCsrfCookie(c) {
2739
2741
  const secret = c.env?.JWT_SECRET || JWT_SECRET_FALLBACK;
2740
2742
  const isDev = c.env?.ENVIRONMENT === "development" || !c.env?.ENVIRONMENT;
2741
- const csrfToken = await chunkDQZVU3WB_cjs.generateCsrfToken(secret);
2743
+ const csrfToken = await chunk5GO3AMON_cjs.generateCsrfToken(secret);
2742
2744
  cookie.setCookie(c, "csrf_token", csrfToken, {
2743
2745
  httpOnly: false,
2744
2746
  secure: !isDev,
@@ -2795,7 +2797,7 @@ var loginSchema = zod.z.object({
2795
2797
  });
2796
2798
  authRoutes.post(
2797
2799
  "/register",
2798
- chunkDQZVU3WB_cjs.rateLimit({ max: 3, windowMs: 60 * 1e3, keyPrefix: "register" }),
2800
+ chunk5GO3AMON_cjs.rateLimit({ max: 3, windowMs: 60 * 1e3, keyPrefix: "register" }),
2799
2801
  async (c) => {
2800
2802
  try {
2801
2803
  const db = c.env.DB;
@@ -2832,7 +2834,7 @@ authRoutes.post(
2832
2834
  if (existingUser) {
2833
2835
  return c.json({ error: "User with this email or username already exists" }, 400);
2834
2836
  }
2835
- const passwordHash = await chunkDQZVU3WB_cjs.AuthManager.hashPassword(password);
2837
+ const passwordHash = await chunk5GO3AMON_cjs.AuthManager.hashPassword(password);
2836
2838
  const userId = crypto.randomUUID();
2837
2839
  const now = /* @__PURE__ */ new Date();
2838
2840
  await db.prepare(`
@@ -2852,7 +2854,7 @@ authRoutes.post(
2852
2854
  now.getTime(),
2853
2855
  now.getTime()
2854
2856
  ).run();
2855
- const token = await chunkDQZVU3WB_cjs.AuthManager.generateToken(userId, normalizedEmail, "viewer", c.env.JWT_SECRET);
2857
+ const token = await chunk5GO3AMON_cjs.AuthManager.generateToken(userId, normalizedEmail, "viewer", c.env.JWT_SECRET);
2856
2858
  cookie.setCookie(c, "auth_token", token, {
2857
2859
  httpOnly: true,
2858
2860
  secure: true,
@@ -2886,7 +2888,7 @@ authRoutes.post(
2886
2888
  );
2887
2889
  authRoutes.post(
2888
2890
  "/login",
2889
- chunkDQZVU3WB_cjs.rateLimit({ max: 5, windowMs: 60 * 1e3, keyPrefix: "login" }),
2891
+ chunk5GO3AMON_cjs.rateLimit({ max: 5, windowMs: 60 * 1e3, keyPrefix: "login" }),
2890
2892
  async (c) => {
2891
2893
  try {
2892
2894
  const body = await c.req.json();
@@ -2897,7 +2899,7 @@ authRoutes.post(
2897
2899
  const { email, password } = validation.data;
2898
2900
  const db = c.env.DB;
2899
2901
  const normalizedEmail = email.toLowerCase();
2900
- const cache = chunk64APW3DW_cjs.getCacheService(chunk64APW3DW_cjs.CACHE_CONFIGS.user);
2902
+ const cache = chunkLFAQUR7P_cjs.getCacheService(chunkLFAQUR7P_cjs.CACHE_CONFIGS.user);
2901
2903
  let user = await cache.get(cache.generateKey("user", `email:${normalizedEmail}`));
2902
2904
  if (!user) {
2903
2905
  user = await db.prepare("SELECT * FROM users WHERE email = ? AND is_active = 1").bind(normalizedEmail).first();
@@ -2909,19 +2911,19 @@ authRoutes.post(
2909
2911
  if (!user) {
2910
2912
  return c.json({ error: "Invalid email or password" }, 401);
2911
2913
  }
2912
- const isValidPassword = await chunkDQZVU3WB_cjs.AuthManager.verifyPassword(password, user.password_hash);
2914
+ const isValidPassword = await chunk5GO3AMON_cjs.AuthManager.verifyPassword(password, user.password_hash);
2913
2915
  if (!isValidPassword) {
2914
2916
  return c.json({ error: "Invalid email or password" }, 401);
2915
2917
  }
2916
- if (chunkDQZVU3WB_cjs.AuthManager.isLegacyHash(user.password_hash)) {
2918
+ if (chunk5GO3AMON_cjs.AuthManager.isLegacyHash(user.password_hash)) {
2917
2919
  try {
2918
- const newHash = await chunkDQZVU3WB_cjs.AuthManager.hashPassword(password);
2920
+ const newHash = await chunk5GO3AMON_cjs.AuthManager.hashPassword(password);
2919
2921
  await db.prepare("UPDATE users SET password_hash = ?, updated_at = ? WHERE id = ?").bind(newHash, Date.now(), user.id).run();
2920
2922
  } catch (rehashError) {
2921
2923
  console.error("Password rehash failed (non-fatal):", rehashError);
2922
2924
  }
2923
2925
  }
2924
- const token = await chunkDQZVU3WB_cjs.AuthManager.generateToken(user.id, user.email, user.role, c.env.JWT_SECRET);
2926
+ const token = await chunk5GO3AMON_cjs.AuthManager.generateToken(user.id, user.email, user.role, c.env.JWT_SECRET);
2925
2927
  cookie.setCookie(c, "auth_token", token, {
2926
2928
  httpOnly: true,
2927
2929
  secure: true,
@@ -2974,7 +2976,7 @@ authRoutes.get("/logout", (c) => {
2974
2976
  clearCsrfCookie(c);
2975
2977
  return c.redirect("/auth/login?message=You have been logged out successfully");
2976
2978
  });
2977
- authRoutes.get("/me", chunkDQZVU3WB_cjs.requireAuth(), async (c) => {
2979
+ authRoutes.get("/me", chunk5GO3AMON_cjs.requireAuth(), async (c) => {
2978
2980
  try {
2979
2981
  const user = c.get("user");
2980
2982
  if (!user) {
@@ -2991,13 +2993,13 @@ authRoutes.get("/me", chunkDQZVU3WB_cjs.requireAuth(), async (c) => {
2991
2993
  return c.json({ error: "Failed to get user" }, 500);
2992
2994
  }
2993
2995
  });
2994
- authRoutes.post("/refresh", chunkDQZVU3WB_cjs.requireAuth(), async (c) => {
2996
+ authRoutes.post("/refresh", chunk5GO3AMON_cjs.requireAuth(), async (c) => {
2995
2997
  try {
2996
2998
  const user = c.get("user");
2997
2999
  if (!user) {
2998
3000
  return c.json({ error: "Not authenticated" }, 401);
2999
3001
  }
3000
- const token = await chunkDQZVU3WB_cjs.AuthManager.generateToken(user.userId, user.email, user.role, c.env.JWT_SECRET);
3002
+ const token = await chunk5GO3AMON_cjs.AuthManager.generateToken(user.userId, user.email, user.role, c.env.JWT_SECRET);
3001
3003
  cookie.setCookie(c, "auth_token", token, {
3002
3004
  httpOnly: true,
3003
3005
  secure: true,
@@ -3014,7 +3016,7 @@ authRoutes.post("/refresh", chunkDQZVU3WB_cjs.requireAuth(), async (c) => {
3014
3016
  });
3015
3017
  authRoutes.post(
3016
3018
  "/register/form",
3017
- chunkDQZVU3WB_cjs.rateLimit({ max: 3, windowMs: 60 * 1e3, keyPrefix: "register" }),
3019
+ chunk5GO3AMON_cjs.rateLimit({ max: 3, windowMs: 60 * 1e3, keyPrefix: "register" }),
3018
3020
  async (c) => {
3019
3021
  try {
3020
3022
  const db = c.env.DB;
@@ -3061,7 +3063,7 @@ authRoutes.post(
3061
3063
  </div>
3062
3064
  `);
3063
3065
  }
3064
- const passwordHash = await chunkDQZVU3WB_cjs.AuthManager.hashPassword(password);
3066
+ const passwordHash = await chunk5GO3AMON_cjs.AuthManager.hashPassword(password);
3065
3067
  const role = isFirstUser ? "admin" : "viewer";
3066
3068
  const userId = crypto.randomUUID();
3067
3069
  const now = /* @__PURE__ */ new Date();
@@ -3081,7 +3083,7 @@ authRoutes.post(
3081
3083
  now.getTime(),
3082
3084
  now.getTime()
3083
3085
  ).run();
3084
- const token = await chunkDQZVU3WB_cjs.AuthManager.generateToken(userId, normalizedEmail, role, c.env.JWT_SECRET);
3086
+ const token = await chunk5GO3AMON_cjs.AuthManager.generateToken(userId, normalizedEmail, role, c.env.JWT_SECRET);
3085
3087
  cookie.setCookie(c, "auth_token", token, {
3086
3088
  httpOnly: true,
3087
3089
  secure: false,
@@ -3114,7 +3116,7 @@ authRoutes.post(
3114
3116
  );
3115
3117
  authRoutes.post(
3116
3118
  "/login/form",
3117
- chunkDQZVU3WB_cjs.rateLimit({ max: 5, windowMs: 60 * 1e3, keyPrefix: "login" }),
3119
+ chunk5GO3AMON_cjs.rateLimit({ max: 5, windowMs: 60 * 1e3, keyPrefix: "login" }),
3118
3120
  async (c) => {
3119
3121
  try {
3120
3122
  const formData = await c.req.formData();
@@ -3138,7 +3140,7 @@ authRoutes.post(
3138
3140
  </div>
3139
3141
  `);
3140
3142
  }
3141
- const isValidPassword = await chunkDQZVU3WB_cjs.AuthManager.verifyPassword(password, user.password_hash);
3143
+ const isValidPassword = await chunk5GO3AMON_cjs.AuthManager.verifyPassword(password, user.password_hash);
3142
3144
  if (!isValidPassword) {
3143
3145
  return c.html(html.html`
3144
3146
  <div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
@@ -3146,15 +3148,15 @@ authRoutes.post(
3146
3148
  </div>
3147
3149
  `);
3148
3150
  }
3149
- if (chunkDQZVU3WB_cjs.AuthManager.isLegacyHash(user.password_hash)) {
3151
+ if (chunk5GO3AMON_cjs.AuthManager.isLegacyHash(user.password_hash)) {
3150
3152
  try {
3151
- const newHash = await chunkDQZVU3WB_cjs.AuthManager.hashPassword(password);
3153
+ const newHash = await chunk5GO3AMON_cjs.AuthManager.hashPassword(password);
3152
3154
  await db.prepare("UPDATE users SET password_hash = ?, updated_at = ? WHERE id = ?").bind(newHash, Date.now(), user.id).run();
3153
3155
  } catch (rehashError) {
3154
3156
  console.error("Password rehash failed (non-fatal):", rehashError);
3155
3157
  }
3156
3158
  }
3157
- const token = await chunkDQZVU3WB_cjs.AuthManager.generateToken(user.id, user.email, user.role, c.env.JWT_SECRET);
3159
+ const token = await chunk5GO3AMON_cjs.AuthManager.generateToken(user.id, user.email, user.role, c.env.JWT_SECRET);
3158
3160
  cookie.setCookie(c, "auth_token", token, {
3159
3161
  httpOnly: true,
3160
3162
  secure: false,
@@ -3196,7 +3198,7 @@ authRoutes.post(
3196
3198
  );
3197
3199
  authRoutes.post(
3198
3200
  "/seed-admin",
3199
- chunkDQZVU3WB_cjs.rateLimit({ max: 2, windowMs: 60 * 1e3, keyPrefix: "seed-admin" }),
3201
+ chunk5GO3AMON_cjs.rateLimit({ max: 2, windowMs: 60 * 1e3, keyPrefix: "seed-admin" }),
3200
3202
  async (c) => {
3201
3203
  try {
3202
3204
  const db = c.env.DB;
@@ -3218,7 +3220,7 @@ authRoutes.post(
3218
3220
  `).run();
3219
3221
  const existingAdmin = await db.prepare("SELECT id FROM users WHERE email = ? OR username = ?").bind("admin@sonicjs.com", "admin").first();
3220
3222
  if (existingAdmin) {
3221
- const passwordHash2 = await chunkDQZVU3WB_cjs.AuthManager.hashPassword("sonicjs!");
3223
+ const passwordHash2 = await chunk5GO3AMON_cjs.AuthManager.hashPassword("sonicjs!");
3222
3224
  await db.prepare("UPDATE users SET password_hash = ?, updated_at = ? WHERE id = ?").bind(passwordHash2, Date.now(), existingAdmin.id).run();
3223
3225
  return c.json({
3224
3226
  message: "Admin user already exists (password updated)",
@@ -3230,7 +3232,7 @@ authRoutes.post(
3230
3232
  }
3231
3233
  });
3232
3234
  }
3233
- const passwordHash = await chunkDQZVU3WB_cjs.AuthManager.hashPassword("sonicjs!");
3235
+ const passwordHash = await chunk5GO3AMON_cjs.AuthManager.hashPassword("sonicjs!");
3234
3236
  const userId = "admin-user-id";
3235
3237
  const now = Date.now();
3236
3238
  const adminEmail = "admin@sonicjs.com".toLowerCase();
@@ -3451,7 +3453,7 @@ authRoutes.post("/accept-invitation", async (c) => {
3451
3453
  if (existingUsername) {
3452
3454
  return c.json({ error: "Username is already taken" }, 400);
3453
3455
  }
3454
- const passwordHash = await chunkDQZVU3WB_cjs.AuthManager.hashPassword(password);
3456
+ const passwordHash = await chunk5GO3AMON_cjs.AuthManager.hashPassword(password);
3455
3457
  const updateStmt = db.prepare(`
3456
3458
  UPDATE users SET
3457
3459
  username = ?,
@@ -3470,7 +3472,7 @@ authRoutes.post("/accept-invitation", async (c) => {
3470
3472
  Date.now(),
3471
3473
  invitedUser.id
3472
3474
  ).run();
3473
- const authToken = await chunkDQZVU3WB_cjs.AuthManager.generateToken(invitedUser.id, invitedUser.email, invitedUser.role, c.env.JWT_SECRET);
3475
+ const authToken = await chunk5GO3AMON_cjs.AuthManager.generateToken(invitedUser.id, invitedUser.email, invitedUser.role, c.env.JWT_SECRET);
3474
3476
  cookie.setCookie(c, "auth_token", authToken, {
3475
3477
  httpOnly: true,
3476
3478
  secure: true,
@@ -3487,7 +3489,7 @@ authRoutes.post("/accept-invitation", async (c) => {
3487
3489
  });
3488
3490
  authRoutes.post(
3489
3491
  "/request-password-reset",
3490
- chunkDQZVU3WB_cjs.rateLimit({ max: 3, windowMs: 15 * 60 * 1e3, keyPrefix: "password-reset" }),
3492
+ chunk5GO3AMON_cjs.rateLimit({ max: 3, windowMs: 15 * 60 * 1e3, keyPrefix: "password-reset" }),
3491
3493
  async (c) => {
3492
3494
  try {
3493
3495
  const formData = await c.req.formData();
@@ -3705,7 +3707,7 @@ authRoutes.post("/reset-password", async (c) => {
3705
3707
  if (Date.now() > user.password_reset_expires) {
3706
3708
  return c.json({ error: "Reset token has expired" }, 400);
3707
3709
  }
3708
- const newPasswordHash = await chunkDQZVU3WB_cjs.AuthManager.hashPassword(password);
3710
+ const newPasswordHash = await chunk5GO3AMON_cjs.AuthManager.hashPassword(password);
3709
3711
  try {
3710
3712
  const historyStmt = db.prepare(`
3711
3713
  INSERT INTO password_history (id, user_id, password_hash, created_at)
@@ -4777,6 +4779,39 @@ function getReadFieldValueScript() {
4777
4779
  window.__sonicReadFieldValueInit = true;
4778
4780
 
4779
4781
  window.sonicReadFieldValue = function(fieldWrapper) {
4782
+ const getDirectChild = (parent, selector) => {
4783
+ if (!(parent instanceof Element)) return null;
4784
+ return Array.from(parent.children).find(
4785
+ (child) => child instanceof Element && child.matches(selector),
4786
+ ) || null;
4787
+ };
4788
+ const getDirectStructuredSubfields = (host) =>
4789
+ Array.from(host.children).filter(
4790
+ (child) => child instanceof Element && child.classList.contains('structured-subfield'),
4791
+ );
4792
+ const getStructuredObjectFieldsHost = (container) => {
4793
+ const directFieldsHost = getDirectChild(container, '[data-structured-object-fields]');
4794
+ if (directFieldsHost) return directFieldsHost;
4795
+ const groupContent = getDirectChild(container, '.field-group-content');
4796
+ const nestedFieldsHost = groupContent
4797
+ ? getDirectChild(groupContent, '[data-structured-object-fields]')
4798
+ : null;
4799
+ if (nestedFieldsHost) return nestedFieldsHost;
4800
+ return getDirectChild(container, '[data-array-item-fields]') || container;
4801
+ };
4802
+ const getDirectStructuredObject = (fieldWrapper) => {
4803
+ const directObject = getDirectChild(fieldWrapper, '[data-structured-object]');
4804
+ if (directObject) return directObject;
4805
+ const formGroup = getDirectChild(fieldWrapper, '.form-group');
4806
+ return formGroup ? getDirectChild(formGroup, '[data-structured-object]') : null;
4807
+ };
4808
+ const getDirectStructuredArray = (fieldWrapper) => {
4809
+ const directArray = getDirectChild(fieldWrapper, '[data-structured-array]');
4810
+ if (directArray) return directArray;
4811
+ const formGroup = getDirectChild(fieldWrapper, '.form-group');
4812
+ return formGroup ? getDirectChild(formGroup, '[data-structured-array]') : null;
4813
+ };
4814
+
4780
4815
  const fieldType = fieldWrapper.dataset.fieldType;
4781
4816
  const select = fieldWrapper.querySelector('select');
4782
4817
  const textarea = fieldWrapper.querySelector('textarea');
@@ -4786,7 +4821,47 @@ function getReadFieldValueScript() {
4786
4821
  const nonHiddenInput = inputs.find((input) => input.type !== 'hidden' && input.type !== 'checkbox');
4787
4822
  const hiddenInput = inputs.find((input) => input.type === 'hidden');
4788
4823
 
4824
+ const readStructuredFieldsHost = (host) => {
4825
+ const fields = getDirectStructuredSubfields(host);
4826
+ if (fields.length === 1 && fields[0].dataset.structuredField === '__value') {
4827
+ return window.sonicReadFieldValue(fields[0]);
4828
+ }
4829
+ return fields.reduce((acc, subfield) => {
4830
+ const fieldName = subfield.dataset.structuredField;
4831
+ if (!fieldName || fieldName === '__value') return acc;
4832
+ acc[fieldName] = window.sonicReadFieldValue(subfield);
4833
+ return acc;
4834
+ }, {});
4835
+ };
4836
+
4837
+ const readStructuredObject = () => {
4838
+ const objectContainer = getDirectStructuredObject(fieldWrapper);
4839
+ if (!objectContainer) return null;
4840
+ const host = getStructuredObjectFieldsHost(objectContainer);
4841
+ return readStructuredFieldsHost(host);
4842
+ };
4843
+
4844
+ const readStructuredArray = () => {
4845
+ const arrayContainer = getDirectStructuredArray(fieldWrapper);
4846
+ if (!arrayContainer) return null;
4847
+ const list = arrayContainer.querySelector('[data-structured-array-list]');
4848
+ if (!list) return [];
4849
+ const items = Array.from(list.querySelectorAll(':scope > .structured-array-item'));
4850
+ return items.map((item) => {
4851
+ const host =
4852
+ item.querySelector(':scope > [data-array-item-fields]') ||
4853
+ item.querySelector('[data-array-item-fields]') ||
4854
+ item;
4855
+ return readStructuredFieldsHost(host);
4856
+ });
4857
+ };
4858
+
4789
4859
  if (fieldType === 'object' || fieldType === 'array') {
4860
+ const liveValue = fieldType === 'array' ? readStructuredArray() : readStructuredObject();
4861
+ if (liveValue !== null) {
4862
+ return liveValue;
4863
+ }
4864
+
4790
4865
  if (!hiddenInput) {
4791
4866
  return fieldType === 'array' ? [] : {};
4792
4867
  }
@@ -4835,6 +4910,15 @@ function getReadFieldValueScript() {
4835
4910
  </script>
4836
4911
  `;
4837
4912
  }
4913
+ var STRUCTURED_INDEX_TOKEN = "__INDEX__";
4914
+ var BLOCK_INDEX_TOKEN = "__BLOCK_INDEX__";
4915
+ function sanitizeStructuredGroupId(fieldName) {
4916
+ return `object-${fieldName}`.split(BLOCK_INDEX_TOKEN).map(
4917
+ (blockSegment) => blockSegment.split(STRUCTURED_INDEX_TOKEN).map(
4918
+ (segment) => segment.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "")
4919
+ ).join(STRUCTURED_INDEX_TOKEN)
4920
+ ).join(BLOCK_INDEX_TOKEN);
4921
+ }
4838
4922
  function isMarkdownEditorFieldType(fieldType) {
4839
4923
  return fieldType === "markdown" || fieldType === "mdxeditor" || fieldType === "easymde";
4840
4924
  }
@@ -5415,12 +5499,14 @@ function renderDynamicField(field, options = {}) {
5415
5499
 
5416
5500
  ${isMultiple ? `
5417
5501
  <div class="media-preview-grid grid grid-cols-4 gap-2 mb-2 ${mediaValues.length === 0 ? "hidden" : ""}" id="${fieldId}-preview">
5418
- ${mediaValues.map((url, idx) => `
5502
+ ${mediaValues.map(
5503
+ (url, idx) => `
5419
5504
  <div class="relative media-preview-item" data-url="${url}">
5420
5505
  ${renderMediaPreview(url, `Media ${idx + 1}`, "w-full h-24 object-cover rounded-lg border border-white/20")}
5421
5506
  <button
5422
5507
  type="button"
5423
5508
  onclick="removeMediaFromMultiple('${fieldId}', '${url}')"
5509
+ data-media-remove="true"
5424
5510
  class="absolute top-1 right-1 bg-red-600 text-white rounded-full p-1 hover:bg-red-700"
5425
5511
  ${disabled ? "disabled" : ""}
5426
5512
  >
@@ -5429,7 +5515,8 @@ function renderDynamicField(field, options = {}) {
5429
5515
  </svg>
5430
5516
  </button>
5431
5517
  </div>
5432
- `).join("")}
5518
+ `
5519
+ ).join("")}
5433
5520
  </div>
5434
5521
  ` : `
5435
5522
  <div class="media-preview ${singleValue ? "" : "hidden"}" id="${fieldId}-preview">
@@ -5453,6 +5540,7 @@ function renderDynamicField(field, options = {}) {
5453
5540
  <button
5454
5541
  type="button"
5455
5542
  onclick="clearMediaField('${fieldId}')"
5543
+ data-media-remove="true"
5456
5544
  class="inline-flex items-center px-4 py-2 bg-red-600 text-white rounded-xl hover:bg-red-700 transition-all"
5457
5545
  ${disabled ? "disabled" : ""}
5458
5546
  >
@@ -5486,7 +5574,7 @@ function renderDynamicField(field, options = {}) {
5486
5574
  }
5487
5575
  const showLabel = field.field_type !== "boolean";
5488
5576
  return `
5489
- <div class="form-group">
5577
+ <div class="form-group" data-has-errors="${errors.length > 0 ? "true" : "false"}">
5490
5578
  ${showLabel ? `
5491
5579
  <label for="${fieldId}" class="block text-sm/6 font-medium text-zinc-950 dark:text-white mb-2">
5492
5580
  ${escapeHtml3(field.field_label)}
@@ -5495,7 +5583,7 @@ function renderDynamicField(field, options = {}) {
5495
5583
  ` : ""}
5496
5584
  ${fieldHTML}
5497
5585
  ${errors.length > 0 ? `
5498
- <div class="mt-2 text-sm text-pink-600 dark:text-pink-400">
5586
+ <div class="mt-2 text-sm text-pink-600 dark:text-pink-400" data-validation-error-message>
5499
5587
  ${errors.map((error) => `<div>${escapeHtml3(error)}</div>`).join("")}
5500
5588
  </div>
5501
5589
  ` : ""}
@@ -5510,8 +5598,8 @@ function renderDynamicField(field, options = {}) {
5510
5598
  function renderFieldGroup(title, fields, collapsible = false) {
5511
5599
  const groupId = title.toLowerCase().replace(/\s+/g, "-");
5512
5600
  return `
5513
- <div class="field-group rounded-lg bg-white dark:bg-zinc-900 shadow-sm ring-1 ring-zinc-950/5 dark:ring-white/10 mb-6">
5514
- <div class="field-group-header border-b border-zinc-950/5 dark:border-white/10 px-6 py-4 ${collapsible ? "cursor-pointer" : ""}" ${collapsible ? `onclick="toggleFieldGroup('${groupId}')"` : ""}>
5601
+ <div class="field-group rounded-lg bg-white dark:bg-zinc-900 shadow-sm ring-1 ring-zinc-950/5 dark:ring-white/10 mb-6" data-group-id="${escapeHtml3(groupId)}">
5602
+ <div class="field-group-header border-b border-zinc-950/5 dark:border-white/10 px-6 py-4 ${collapsible ? "cursor-pointer" : ""}" ${collapsible ? `onclick="toggleFieldGroup(this)"` : ""}>
5515
5603
  <h3 class="text-base/7 font-semibold text-zinc-950 dark:text-white flex items-center">
5516
5604
  ${escapeHtml3(title)}
5517
5605
  ${collapsible ? `
@@ -5555,6 +5643,12 @@ function renderBlocksField(field, options, baseClasses, errorClasses) {
5555
5643
  >
5556
5644
  <input type="hidden" id="${fieldId}" name="${fieldName}" value="${escapeHtml3(JSON.stringify(blockValues))}">
5557
5645
 
5646
+ <div class="flex items-center justify-between border-b border-zinc-950/5 dark:border-white/10 py-4">
5647
+ <h3 class="text-base/7 font-semibold text-zinc-950 dark:text-white">
5648
+ ${escapeHtml3(field.field_label || "Content Blocks")}
5649
+ </h3>
5650
+ </div>
5651
+
5558
5652
  <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
5559
5653
  <div class="flex-1">
5560
5654
  <select
@@ -5585,12 +5679,14 @@ function renderBlocksField(field, options, baseClasses, errorClasses) {
5585
5679
  `;
5586
5680
  }
5587
5681
  function renderStructuredObjectField(field, options, baseClasses, errorClasses) {
5588
- const { value = {}, pluginStatuses = {} } = options;
5682
+ const { value = {}, pluginStatuses = {}, errors = [] } = options;
5589
5683
  const opts = field.field_options || {};
5590
5684
  const properties = opts.properties && typeof opts.properties === "object" ? opts.properties : {};
5591
5685
  const fieldId = `field-${field.field_name}`;
5592
5686
  const fieldName = field.field_name;
5593
5687
  const objectValue = normalizeStructuredObjectValue(value);
5688
+ const objectLayout = opts.objectLayout || "nested";
5689
+ const useNestedLayout = objectLayout !== "flat";
5594
5690
  const subfields = Object.entries(properties).map(
5595
5691
  ([propertyName, propertyConfig]) => renderStructuredSubfield(
5596
5692
  field,
@@ -5601,11 +5697,40 @@ function renderStructuredObjectField(field, options, baseClasses, errorClasses)
5601
5697
  field.field_name
5602
5698
  )
5603
5699
  ).join("");
5700
+ const groupTitle = field.field_label || field.field_name;
5701
+ if (!useNestedLayout) {
5702
+ return `
5703
+ <div class="space-y-4" data-structured-object data-field-name="${escapeHtml3(fieldName)}">
5704
+ <input type="hidden" id="${fieldId}" name="${fieldName}" value="${escapeHtml3(JSON.stringify(objectValue))}">
5705
+ <div class="flex items-center justify-between border-b border-zinc-950/5 dark:border-white/10 py-4 first-of-type:pt-0">
5706
+ <h3 class="text-base/7 font-semibold text-zinc-950 dark:text-white">
5707
+ ${escapeHtml3(groupTitle)}
5708
+ </h3>
5709
+ </div>
5710
+ <div class="space-y-4" data-structured-object-fields>
5711
+ ${subfields}
5712
+ </div>
5713
+ </div>
5714
+ ${getStructuredFieldScript()}
5715
+ `;
5716
+ }
5717
+ const groupId = sanitizeStructuredGroupId(field.field_name);
5718
+ const isCollapsed = errors.length > 0 ? false : opts.collapsed !== false;
5604
5719
  return `
5605
- <div class="space-y-4" data-structured-object data-field-name="${escapeHtml3(fieldName)}">
5606
- <input type="hidden" id="${fieldId}" name="${fieldName}" value="${escapeHtml3(JSON.stringify(objectValue))}">
5607
- <div class="space-y-4" data-structured-object-fields>
5608
- ${subfields}
5720
+ <div class="field-group rounded-lg shadow-sm mb-6" data-group-id="${escapeHtml3(groupId)}" data-structured-object data-field-name="${escapeHtml3(fieldName)}">
5721
+ <div class="field-group-header border-b border-zinc-950/5 dark:border-white/10 pr-6 pb-4 cursor-pointer" onclick="toggleFieldGroup(this)">
5722
+ <h3 class="text-base/7 font-semibold text-zinc-950 dark:text-white flex items-center">
5723
+ ${escapeHtml3(groupTitle)}
5724
+ <svg id="${groupId}-icon" class="w-5 h-5 ml-2 transform transition-transform ${isCollapsed ? "-rotate-90" : ""} text-zinc-500 dark:text-zinc-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
5725
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
5726
+ </svg>
5727
+ </h3>
5728
+ </div>
5729
+ <div id="${groupId}-content" class="field-group-content px-6 py-6 space-y-4 ${isCollapsed ? "hidden" : ""}">
5730
+ <input type="hidden" id="${fieldId}" name="${fieldName}" value="${escapeHtml3(JSON.stringify(objectValue))}">
5731
+ <div class="space-y-4" data-structured-object-fields>
5732
+ ${subfields}
5733
+ </div>
5609
5734
  </div>
5610
5735
  </div>
5611
5736
  ${getStructuredFieldScript()}
@@ -5676,7 +5801,7 @@ function renderStructuredArrayField(field, options, baseClasses, errorClasses) {
5676
5801
  function renderStructuredArrayItem(field, itemConfig, index, itemValue, pluginStatuses, arrayItemTitle) {
5677
5802
  const itemFields = renderStructuredItemFields(field, itemConfig, index, itemValue, pluginStatuses);
5678
5803
  return `
5679
- <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="${escapeHtml3(index)}" draggable="true">
5804
+ <div class="structured-array-item rounded-lg border border-zinc-200 dark:border-white/10 bg-white/60 dark:bg-zinc-600/5 p-4 shadow-lg shadow-zinc-950/20" data-array-index="${escapeHtml3(index)}" draggable="true">
5680
5805
  <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
5681
5806
  <div class="flex items-center gap-3">
5682
5807
  <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">
@@ -5689,6 +5814,11 @@ function renderStructuredArrayItem(field, itemConfig, index, itemValue, pluginSt
5689
5814
  </div>
5690
5815
  </div>
5691
5816
  <div class="flex flex-wrap gap-2 text-xs">
5817
+ <button type="button" data-action="toggle-item" 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" aria-label="Expand item" title="Expand">
5818
+ <svg class="h-4 w-4 transition-transform -rotate-90 text-zinc-500 dark:text-zinc-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" data-item-toggle-icon>
5819
+ <path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7"></path>
5820
+ </svg>
5821
+ </button>
5692
5822
  <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">
5693
5823
  <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="4">
5694
5824
  <path stroke-linecap="round" stroke-linejoin="round" d="M12 6l-4 4m4-4l4 4m-4-4v12"/>
@@ -5707,7 +5837,7 @@ function renderStructuredArrayItem(field, itemConfig, index, itemValue, pluginSt
5707
5837
  </button>
5708
5838
  </div>
5709
5839
  </div>
5710
- <div class="mt-4 space-y-4" data-array-item-fields>
5840
+ <div class="mt-4 space-y-4 hidden" data-array-item-fields>
5711
5841
  ${itemFields}
5712
5842
  </div>
5713
5843
  </div>
@@ -5817,7 +5947,7 @@ function normalizeBlocksValue(value, discriminator) {
5817
5947
  function renderBlockTemplate(field, block, discriminator, pluginStatuses) {
5818
5948
  return `
5819
5949
  <template data-block-template="${escapeHtml3(block.name)}">
5820
- ${renderBlockCard(field, block, discriminator, "__INDEX__", {}, pluginStatuses)}
5950
+ ${renderBlockCard(field, block, discriminator, BLOCK_INDEX_TOKEN, {}, pluginStatuses)}
5821
5951
  </template>
5822
5952
  `;
5823
5953
  }
@@ -5859,7 +5989,7 @@ function renderBlockCard(field, block, discriminator, index, data, pluginStatuse
5859
5989
  `;
5860
5990
  }).join("");
5861
5991
  return `
5862
- <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="${escapeHtml3(block.name)}" data-block-discriminator="${escapeHtml3(discriminator)}" draggable="true">
5992
+ <div class="blocks-item rounded-lg border border-zinc-200 dark:border-white/10 dark:bg-zinc-600/5 p-4 shadow-lg shadow-zinc-950/20" data-block-type="${escapeHtml3(block.name)}" data-block-discriminator="${escapeHtml3(discriminator)}" draggable="true">
5863
5993
  <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
5864
5994
  <div class="flex items-start gap-3">
5865
5995
  <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">
@@ -5867,7 +5997,7 @@ function renderBlockCard(field, block, discriminator, index, data, pluginStatuse
5867
5997
  <path stroke-linecap="round" stroke-linejoin="round" d="M4 8h16M4 16h16"/>
5868
5998
  </svg>
5869
5999
  </div>
5870
- <div>
6000
+ <div class="cursor-pointer" data-action="toggle-block">
5871
6001
  <div class="text-sm font-semibold text-zinc-900 dark:text-white">
5872
6002
  ${escapeHtml3(block.label)}
5873
6003
  <span class="ml-2 text-xs font-normal text-zinc-500 dark:text-zinc-400" data-block-order-label></span>
@@ -5876,6 +6006,11 @@ function renderBlockCard(field, block, discriminator, index, data, pluginStatuse
5876
6006
  </div>
5877
6007
  </div>
5878
6008
  <div class="flex flex-wrap gap-2 text-xs">
6009
+ <button type="button" data-action="toggle-block" 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" aria-label="Expand block" title="Expand">
6010
+ <svg class="h-4 w-4 transition-transform -rotate-90 text-zinc-500 dark:text-zinc-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" data-block-toggle-icon>
6011
+ <path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7"></path>
6012
+ </svg>
6013
+ </button>
5879
6014
  <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">
5880
6015
  <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="4">
5881
6016
  <path stroke-linecap="round" stroke-linejoin="round" d="M12 6l-4 4m4-4l4 4m-4-4v12"/>
@@ -5894,7 +6029,7 @@ function renderBlockCard(field, block, discriminator, index, data, pluginStatuse
5894
6029
  </button>
5895
6030
  </div>
5896
6031
  </div>
5897
- <div class="mt-4 space-y-4">
6032
+ <div class="mt-4 space-y-4 hidden" data-block-content>
5898
6033
  ${blockFields}
5899
6034
  </div>
5900
6035
  </div>
@@ -5928,9 +6063,101 @@ function getStructuredFieldScript() {
5928
6063
 
5929
6064
  function initializeStructuredFields() {
5930
6065
  const readFieldValue = window.sonicReadFieldValue;
6066
+ const getDirectChild = (parent, selector) => {
6067
+ if (!(parent instanceof Element)) return null;
6068
+ return Array.from(parent.children).find(
6069
+ (child) => child instanceof Element && child.matches(selector),
6070
+ ) || null;
6071
+ };
6072
+ const getDirectStructuredSubfields = (host) =>
6073
+ Array.from(host.children).filter(
6074
+ (child) => child instanceof Element && child.classList.contains('structured-subfield'),
6075
+ );
6076
+ const getStructuredValueHost = (container) => {
6077
+ const directObjectHost = getDirectChild(container, '[data-structured-object-fields]');
6078
+ if (directObjectHost) return directObjectHost;
6079
+ const groupContent = getDirectChild(container, '.field-group-content');
6080
+ const nestedObjectHost = groupContent
6081
+ ? getDirectChild(groupContent, '[data-structured-object-fields]')
6082
+ : null;
6083
+ if (nestedObjectHost) return nestedObjectHost;
6084
+ return getDirectChild(container, '[data-array-item-fields]') || container;
6085
+ };
6086
+ const getCollectionScope = () => {
6087
+ const url = new URL(window.location.href);
6088
+ const collectionFromQuery = url.searchParams.get('collection');
6089
+ const form = document.getElementById('content-form');
6090
+ const collectionInput = form?.querySelector('input[name="collection_id"]');
6091
+ const collectionFromForm = collectionInput instanceof HTMLInputElement ? collectionInput.value : '';
6092
+ const collectionId = collectionFromQuery || collectionFromForm || '';
6093
+ return window.location.pathname + ':' + collectionId;
6094
+ };
6095
+
6096
+ const getArrayStateKey = (container) => {
6097
+ const fieldName = container.dataset.fieldName || 'unknown';
6098
+ return 'sonic:ui:repeaters:' + getCollectionScope() + ':' + fieldName;
6099
+ };
6100
+
6101
+ const readArrayState = (container) => {
6102
+ try {
6103
+ const raw = sessionStorage.getItem(getArrayStateKey(container));
6104
+ if (!raw) return null;
6105
+ const parsed = JSON.parse(raw);
6106
+ return Array.isArray(parsed) ? parsed : null;
6107
+ } catch {
6108
+ return null;
6109
+ }
6110
+ };
6111
+
6112
+ const writeArrayState = (container, state) => {
6113
+ try {
6114
+ sessionStorage.setItem(getArrayStateKey(container), JSON.stringify(state));
6115
+ } catch {}
6116
+ };
6117
+
6118
+ const setArrayItemExpanded = (item, isExpanded) => {
6119
+ const content = item.querySelector('[data-array-item-fields]');
6120
+ const icon = item.querySelector('[data-item-toggle-icon]');
6121
+ if (content instanceof HTMLElement) {
6122
+ content.classList.toggle('hidden', !isExpanded);
6123
+ }
6124
+ if (icon instanceof Element) {
6125
+ icon.classList.toggle('-rotate-90', !isExpanded);
6126
+ }
6127
+ };
6128
+
6129
+ const getArrayItems = (container, list) => {
6130
+ if (list) {
6131
+ return Array.from(list.querySelectorAll(':scope > .structured-array-item'));
6132
+ }
6133
+ return Array.from(
6134
+ container.querySelectorAll(':scope > [data-structured-array-list] > .structured-array-item'),
6135
+ );
6136
+ };
6137
+
6138
+ const captureArrayState = (container) => {
6139
+ return getArrayItems(container).map((item) => {
6140
+ const content = item.querySelector('[data-array-item-fields]');
6141
+ return content instanceof HTMLElement ? !content.classList.contains('hidden') : false;
6142
+ });
6143
+ };
6144
+
6145
+ const applyArrayState = (container, state) => {
6146
+ const items = getArrayItems(container);
6147
+ items.forEach((item, index) => {
6148
+ if (typeof state[index] === 'boolean') {
6149
+ setArrayItemExpanded(item, state[index]);
6150
+ }
6151
+ });
6152
+ };
6153
+
6154
+ const syncArrayState = (container) => {
6155
+ writeArrayState(container, captureArrayState(container));
6156
+ };
5931
6157
 
5932
6158
  const readStructuredValue = (container) => {
5933
- const fields = Array.from(container.querySelectorAll('.structured-subfield'));
6159
+ const fieldHost = getStructuredValueHost(container);
6160
+ const fields = getDirectStructuredSubfields(fieldHost);
5934
6161
  if (fields.length === 1 && fields[0].dataset.structuredField === '__value') {
5935
6162
  return readFieldValue(fields[0]);
5936
6163
  }
@@ -5944,111 +6171,229 @@ function getStructuredFieldScript() {
5944
6171
  };
5945
6172
 
5946
6173
  document.querySelectorAll('[data-structured-object]').forEach((container) => {
6174
+ if (container.closest('template')) {
6175
+ return;
6176
+ }
5947
6177
  if (container.dataset.structuredInitialized === 'true') {
5948
6178
  return;
5949
6179
  }
5950
- container.dataset.structuredInitialized = 'true';
5951
- const hiddenInput = container.querySelector('input[type="hidden"]');
6180
+ if (container.dataset.structuredInitializing === 'true') {
6181
+ return;
6182
+ }
6183
+ container.dataset.structuredInitializing = 'true';
6184
+ try {
6185
+ const hiddenInput = container.querySelector('input[type="hidden"]');
5952
6186
 
5953
- const updateHiddenInput = () => {
5954
- if (!hiddenInput) return;
5955
- const value = readStructuredValue(container);
5956
- hiddenInput.value = JSON.stringify(value);
5957
- };
6187
+ const updateHiddenInput = () => {
6188
+ if (!hiddenInput) return;
6189
+ const value = readStructuredValue(container);
6190
+ hiddenInput.value = JSON.stringify(value);
6191
+ };
5958
6192
 
5959
- container.addEventListener('input', updateHiddenInput);
5960
- container.addEventListener('change', updateHiddenInput);
5961
- updateHiddenInput();
6193
+ container.addEventListener('input', updateHiddenInput);
6194
+ container.addEventListener('change', updateHiddenInput);
6195
+ updateHiddenInput();
6196
+ container.dataset.structuredInitialized = 'true';
6197
+ } catch (error) {
6198
+ delete container.dataset.structuredInitialized;
6199
+ console.error('[structured-object] initialization failed', error);
6200
+ } finally {
6201
+ delete container.dataset.structuredInitializing;
6202
+ }
5962
6203
  });
5963
6204
 
5964
6205
  document.querySelectorAll('[data-structured-array]').forEach((container) => {
6206
+ if (container.closest('template')) {
6207
+ return;
6208
+ }
5965
6209
  if (container.dataset.structuredInitialized === 'true') {
5966
6210
  return;
5967
6211
  }
5968
- container.dataset.structuredInitialized = 'true';
5969
- const list = container.querySelector('[data-structured-array-list]');
5970
- const hiddenInput = container.querySelector('input[type="hidden"]');
5971
- const template = container.querySelector('template[data-structured-array-template]');
6212
+ if (container.dataset.structuredInitializing === 'true') {
6213
+ return;
6214
+ }
6215
+ container.dataset.structuredInitializing = 'true';
6216
+ try {
6217
+ const list = container.querySelector(':scope > [data-structured-array-list]');
6218
+ const hiddenInput = container.querySelector(':scope > input[type="hidden"]');
6219
+ const template = container.querySelector(':scope > template[data-structured-array-template]');
6220
+ if (
6221
+ template instanceof HTMLTemplateElement &&
6222
+ typeof template.innerHTML === 'string' &&
6223
+ template.innerHTML.trim()
6224
+ ) {
6225
+ container.__sonicStructuredArrayTemplate = template.innerHTML;
6226
+ }
5972
6227
 
5973
- const updateOrderLabels = () => {
5974
- const items = Array.from(container.querySelectorAll('.structured-array-item'));
5975
- items.forEach((item, index) => {
5976
- const label = item.querySelector('[data-array-order-label]');
5977
- if (label) {
5978
- label.textContent = '#'+ (index + 1);
6228
+ const getLiveList = () =>
6229
+ list || container.querySelector(':scope > [data-structured-array-list]');
6230
+ const getLiveHiddenInput = () =>
6231
+ hiddenInput || container.querySelector(':scope > input[type="hidden"]');
6232
+ const getTemplateHtml = () => {
6233
+ if (typeof container.__sonicStructuredArrayTemplate === 'string' &&
6234
+ container.__sonicStructuredArrayTemplate.trim()) {
6235
+ return container.__sonicStructuredArrayTemplate;
5979
6236
  }
5980
6237
 
5981
- const moveUpButton = item.querySelector('[data-action="move-up"]');
5982
- if (moveUpButton instanceof HTMLButtonElement) {
5983
- moveUpButton.disabled = index === 0;
6238
+ const liveTemplate =
6239
+ template instanceof HTMLTemplateElement
6240
+ ? template
6241
+ : container.querySelector(':scope > template[data-structured-array-template]');
6242
+ if (
6243
+ liveTemplate instanceof HTMLTemplateElement &&
6244
+ typeof liveTemplate.innerHTML === 'string' &&
6245
+ liveTemplate.innerHTML.trim()
6246
+ ) {
6247
+ container.__sonicStructuredArrayTemplate = liveTemplate.innerHTML;
6248
+ return liveTemplate.innerHTML;
5984
6249
  }
6250
+ return typeof container.__sonicStructuredArrayTemplate === 'string'
6251
+ ? container.__sonicStructuredArrayTemplate
6252
+ : '';
6253
+ };
5985
6254
 
5986
- const moveDownButton = item.querySelector('[data-action="move-down"]');
5987
- if (moveDownButton instanceof HTMLButtonElement) {
5988
- moveDownButton.disabled = index === items.length - 1;
6255
+ const updateOrderLabels = () => {
6256
+ const liveList = getLiveList();
6257
+ if (!liveList) return;
6258
+ const items = getArrayItems(container, liveList);
6259
+ items.forEach((item, index) => {
6260
+ const label = item.querySelector('[data-array-order-label]');
6261
+ if (label) {
6262
+ label.textContent = '#'+ (index + 1);
6263
+ }
6264
+
6265
+ const moveUpButton = item.querySelector('[data-action="move-up"]');
6266
+ if (moveUpButton instanceof HTMLButtonElement) {
6267
+ moveUpButton.disabled = index === 0;
6268
+ }
6269
+
6270
+ const moveDownButton = item.querySelector('[data-action="move-down"]');
6271
+ if (moveDownButton instanceof HTMLButtonElement) {
6272
+ moveDownButton.disabled = index === items.length - 1;
6273
+ }
6274
+ });
6275
+ };
6276
+
6277
+ const updateHiddenInput = () => {
6278
+ const liveHiddenInput = getLiveHiddenInput();
6279
+ const liveList = getLiveList();
6280
+ if (!liveHiddenInput || !liveList) return;
6281
+ const items = getArrayItems(container, liveList);
6282
+ const values = items.map((item) => readStructuredValue(item));
6283
+ liveHiddenInput.value = JSON.stringify(values);
6284
+ // Notify parent structured containers after non-input actions (add/remove/move)
6285
+ // so nested array mutations are persisted correctly.
6286
+ liveHiddenInput.dispatchEvent(new Event('change', { bubbles: true }));
6287
+
6288
+ const emptyState = liveList.querySelector(':scope > [data-structured-empty]');
6289
+ if (emptyState) {
6290
+ emptyState.style.display = values.length === 0 ? 'block' : 'none';
5989
6291
  }
5990
- });
5991
- };
6292
+ updateOrderLabels();
6293
+ };
5992
6294
 
5993
- const updateHiddenInput = () => {
5994
- if (!hiddenInput || !list) return;
5995
- const items = Array.from(list.querySelectorAll('.structured-array-item'));
5996
- const values = items.map((item) => readStructuredValue(item));
5997
- hiddenInput.value = JSON.stringify(values);
6295
+ const addArrayItem = () => {
6296
+ const liveList = getLiveList();
6297
+ if (!liveList) return;
6298
+ const templateHtml = getTemplateHtml();
6299
+ if (!templateHtml) return;
6300
+ try {
6301
+ const nextIndex = getArrayItems(container, liveList).length;
6302
+ const html = templateHtml.replace(/__INDEX__/g, String(nextIndex));
6303
+ liveList.insertAdjacentHTML('beforeend', html);
6304
+ const newItem = liveList.lastElementChild;
6305
+ if (newItem instanceof HTMLElement) {
6306
+ // Ensure cloned template content can be initialized even if stale
6307
+ // data-structured-initialized attributes were copied.
6308
+ newItem
6309
+ .querySelectorAll('[data-structured-object], [data-structured-array]')
6310
+ .forEach((nestedContainer) => {
6311
+ if (nestedContainer instanceof HTMLElement) {
6312
+ delete nestedContainer.dataset.structuredInitialized;
6313
+ }
6314
+ });
6315
+ setArrayItemExpanded(newItem, true);
6316
+ }
6317
+ if (typeof initializeTinyMCE === 'function') {
6318
+ initializeTinyMCE();
6319
+ }
6320
+ if (typeof window.initializeQuillEditors === 'function') {
6321
+ window.initializeQuillEditors();
6322
+ }
6323
+ if (typeof initializeMDXEditor === 'function') {
6324
+ initializeMDXEditor();
6325
+ }
6326
+ if (typeof window.initializeStructuredFields === 'function') {
6327
+ window.initializeStructuredFields();
6328
+ }
6329
+ updateHiddenInput();
6330
+ syncArrayState(container);
6331
+ } catch (error) {
6332
+ console.error('[structured-array] add-item failed', error);
6333
+ }
6334
+ };
5998
6335
 
5999
- const emptyState = list.querySelector('[data-structured-empty]');
6000
- if (emptyState) {
6001
- emptyState.style.display = values.length === 0 ? 'block' : 'none';
6336
+ const topLevelAddButton = container.querySelector(
6337
+ ':scope > .flex.items-center.justify-between.gap-3 [data-action="add-item"]',
6338
+ );
6339
+ if (topLevelAddButton instanceof HTMLElement) {
6340
+ topLevelAddButton.addEventListener('click', (event) => {
6341
+ event.preventDefault();
6342
+ event.stopPropagation();
6343
+ addArrayItem();
6344
+ });
6002
6345
  }
6003
- updateOrderLabels();
6004
- };
6005
6346
 
6006
- if (typeof window.initializeDragSortable === 'function' && list) {
6007
- window.initializeDragSortable(list, {
6008
- itemSelector: '.structured-array-item',
6009
- handleSelector: '[data-action="drag-handle"]',
6010
- onUpdate: updateHiddenInput
6011
- });
6012
- }
6347
+ const dragList = getLiveList();
6348
+ if (typeof window.initializeDragSortable === 'function' && dragList) {
6349
+ window.initializeDragSortable(dragList, {
6350
+ itemSelector: '.structured-array-item',
6351
+ handleSelector: '[data-action="drag-handle"]',
6352
+ onUpdate: () => {
6353
+ updateHiddenInput();
6354
+ syncArrayState(container);
6355
+ }
6356
+ });
6357
+ }
6013
6358
 
6014
- container.addEventListener('click', (event) => {
6359
+ container.addEventListener('click', (event) => {
6015
6360
  const target = event.target;
6016
6361
  if (!(target instanceof Element)) return;
6017
6362
  const actionButton = target.closest('[data-action]');
6018
6363
  if (!actionButton || actionButton.hasAttribute('disabled')) return;
6364
+ const actionOwner = actionButton.closest('[data-structured-array]');
6365
+ if (actionOwner !== container) return;
6019
6366
 
6020
- const action = actionButton.getAttribute('data-action');
6367
+ const action = actionButton.getAttribute('data-action');
6021
6368
 
6022
- if (action === 'add-item') {
6023
- if (!list || !template) return;
6024
- const nextIndex = list.querySelectorAll('.structured-array-item').length;
6025
- const html = template.innerHTML.replace(/__INDEX__/g, String(nextIndex));
6026
- list.insertAdjacentHTML('beforeend', html);
6027
- if (typeof initializeTinyMCE === 'function') {
6028
- initializeTinyMCE();
6029
- }
6030
- if (typeof window.initializeQuillEditors === 'function') {
6031
- window.initializeQuillEditors();
6032
- }
6033
- if (typeof initializeMDXEditor === 'function') {
6034
- initializeMDXEditor();
6369
+ if (action === 'add-item') {
6370
+ addArrayItem();
6371
+ return;
6035
6372
  }
6036
- updateHiddenInput();
6037
- return;
6038
- }
6039
6373
 
6040
6374
  const item = actionButton.closest('.structured-array-item');
6041
- if (!item || !list) return;
6375
+ const liveList = getLiveList();
6376
+ if (!item || !liveList) return;
6377
+
6378
+ if (action === 'toggle-item') {
6379
+ const content = item.querySelector('[data-array-item-fields]');
6380
+ if (!(content instanceof HTMLElement)) return;
6381
+ setArrayItemExpanded(item, content.classList.contains('hidden'));
6382
+ syncArrayState(container);
6383
+ return;
6384
+ }
6042
6385
 
6043
6386
  if (action === 'remove-item') {
6044
6387
  if (typeof requestRepeaterDelete === 'function') {
6045
6388
  requestRepeaterDelete(() => {
6046
6389
  item.remove();
6047
6390
  updateHiddenInput();
6391
+ syncArrayState(container);
6048
6392
  });
6049
6393
  } else {
6050
6394
  item.remove();
6051
6395
  updateHiddenInput();
6396
+ syncArrayState(container);
6052
6397
  }
6053
6398
  return;
6054
6399
  }
@@ -6056,8 +6401,9 @@ function getStructuredFieldScript() {
6056
6401
  if (action === 'move-up') {
6057
6402
  const previous = item.previousElementSibling;
6058
6403
  if (previous) {
6059
- list.insertBefore(item, previous);
6404
+ liveList.insertBefore(item, previous);
6060
6405
  updateHiddenInput();
6406
+ syncArrayState(container);
6061
6407
  }
6062
6408
  return;
6063
6409
  }
@@ -6065,29 +6411,43 @@ function getStructuredFieldScript() {
6065
6411
  if (action === 'move-down') {
6066
6412
  const next = item.nextElementSibling;
6067
6413
  if (next) {
6068
- list.insertBefore(next, item);
6414
+ liveList.insertBefore(next, item);
6069
6415
  updateHiddenInput();
6416
+ syncArrayState(container);
6070
6417
  }
6071
6418
  }
6072
- });
6419
+ });
6073
6420
 
6074
- container.addEventListener('input', (event) => {
6421
+ container.addEventListener('input', (event) => {
6075
6422
  const target = event.target;
6076
6423
  if (!(target instanceof Element)) return;
6077
6424
  if (target.closest('[data-structured-array-list]')) {
6078
6425
  updateHiddenInput();
6079
6426
  }
6080
- });
6427
+ });
6081
6428
 
6082
- container.addEventListener('change', (event) => {
6429
+ container.addEventListener('change', (event) => {
6083
6430
  const target = event.target;
6084
6431
  if (!(target instanceof Element)) return;
6085
6432
  if (target.closest('[data-structured-array-list]')) {
6086
6433
  updateHiddenInput();
6087
6434
  }
6088
- });
6435
+ });
6089
6436
 
6090
- updateHiddenInput();
6437
+ updateHiddenInput();
6438
+ const savedArrayState = readArrayState(container);
6439
+ if (savedArrayState) {
6440
+ applyArrayState(container, savedArrayState);
6441
+ } else {
6442
+ syncArrayState(container);
6443
+ }
6444
+ container.dataset.structuredInitialized = 'true';
6445
+ } catch (error) {
6446
+ delete container.dataset.structuredInitialized;
6447
+ console.error('[structured-array] initialization failed', error);
6448
+ } finally {
6449
+ delete container.dataset.structuredInitializing;
6450
+ }
6091
6451
  });
6092
6452
  }
6093
6453
 
@@ -6102,7 +6462,10 @@ function getStructuredFieldScript() {
6102
6462
  document.addEventListener('htmx:afterSwap', function() {
6103
6463
  setTimeout(initializeStructuredFields, 50);
6104
6464
  });
6105
- } else if (typeof window.initializeStructuredFields === 'function') {
6465
+ } else if (
6466
+ typeof window.initializeStructuredFields === 'function' &&
6467
+ document.readyState !== 'loading'
6468
+ ) {
6106
6469
  window.initializeStructuredFields();
6107
6470
  }
6108
6471
  </script>
@@ -6114,6 +6477,68 @@ function getBlocksFieldScript() {
6114
6477
  <script>
6115
6478
  if (!window.__sonicBlocksFieldInit) {
6116
6479
  window.__sonicBlocksFieldInit = true;
6480
+ const getCollectionScope = () => {
6481
+ const url = new URL(window.location.href);
6482
+ const collectionFromQuery = url.searchParams.get('collection');
6483
+ const form = document.getElementById('content-form');
6484
+ const collectionInput = form?.querySelector('input[name="collection_id"]');
6485
+ const collectionFromForm = collectionInput instanceof HTMLInputElement ? collectionInput.value : '';
6486
+ const collectionId = collectionFromQuery || collectionFromForm || '';
6487
+ return window.location.pathname + ':' + collectionId;
6488
+ };
6489
+
6490
+ const getBlocksStateKey = (container) => {
6491
+ const fieldName = container.dataset.fieldName || 'unknown';
6492
+ return 'sonic:ui:blocks:' + getCollectionScope() + ':' + fieldName;
6493
+ };
6494
+
6495
+ const readBlocksState = (container) => {
6496
+ try {
6497
+ const raw = sessionStorage.getItem(getBlocksStateKey(container));
6498
+ if (!raw) return null;
6499
+ const parsed = JSON.parse(raw);
6500
+ return Array.isArray(parsed) ? parsed : null;
6501
+ } catch {
6502
+ return null;
6503
+ }
6504
+ };
6505
+
6506
+ const writeBlocksState = (container, state) => {
6507
+ try {
6508
+ sessionStorage.setItem(getBlocksStateKey(container), JSON.stringify(state));
6509
+ } catch {}
6510
+ };
6511
+
6512
+ const setBlockExpanded = (item, isExpanded) => {
6513
+ const content = item.querySelector('[data-block-content]');
6514
+ const icon = item.querySelector('[data-block-toggle-icon]');
6515
+ if (content instanceof HTMLElement) {
6516
+ content.classList.toggle('hidden', !isExpanded);
6517
+ }
6518
+ if (icon instanceof Element) {
6519
+ icon.classList.toggle('-rotate-90', !isExpanded);
6520
+ }
6521
+ };
6522
+
6523
+ const captureBlocksState = (container) => {
6524
+ return Array.from(container.querySelectorAll('.blocks-item')).map((item) => {
6525
+ const content = item.querySelector('[data-block-content]');
6526
+ return content instanceof HTMLElement ? !content.classList.contains('hidden') : false;
6527
+ });
6528
+ };
6529
+
6530
+ const applyBlocksState = (container, state) => {
6531
+ const items = Array.from(container.querySelectorAll('.blocks-item'));
6532
+ items.forEach((item, index) => {
6533
+ if (typeof state[index] === 'boolean') {
6534
+ setBlockExpanded(item, state[index]);
6535
+ }
6536
+ });
6537
+ };
6538
+
6539
+ const syncBlocksState = (container) => {
6540
+ writeBlocksState(container, captureBlocksState(container));
6541
+ };
6117
6542
 
6118
6543
  function initializeBlocksFields() {
6119
6544
  document.querySelectorAll('.blocks-field').forEach((container) => {
@@ -6201,7 +6626,10 @@ function getBlocksFieldScript() {
6201
6626
  window.initializeDragSortable(list, {
6202
6627
  itemSelector: '.blocks-item',
6203
6628
  handleSelector: '[data-action="drag-handle"]',
6204
- onUpdate: updateHiddenInput
6629
+ onUpdate: () => {
6630
+ updateHiddenInput();
6631
+ syncBlocksState(container);
6632
+ }
6205
6633
  });
6206
6634
  }
6207
6635
 
@@ -6223,8 +6651,12 @@ function getBlocksFieldScript() {
6223
6651
  if (!template) return;
6224
6652
 
6225
6653
  const nextIndex = list.querySelectorAll('.blocks-item').length;
6226
- const html = template.innerHTML.replace(/__INDEX__/g, String(nextIndex));
6654
+ const html = template.innerHTML.replace(/__BLOCK_INDEX__/g, String(nextIndex));
6227
6655
  list.insertAdjacentHTML('beforeend', html);
6656
+ const newItem = list.lastElementChild;
6657
+ if (newItem instanceof HTMLElement) {
6658
+ setBlockExpanded(newItem, true);
6659
+ }
6228
6660
  if (typeSelect) {
6229
6661
  typeSelect.value = '';
6230
6662
  }
@@ -6233,21 +6665,32 @@ function getBlocksFieldScript() {
6233
6665
  window.initializeStructuredFields();
6234
6666
  }
6235
6667
  updateHiddenInput();
6668
+ syncBlocksState(container);
6236
6669
  return;
6237
6670
  }
6238
6671
 
6239
6672
  const item = actionButton.closest('.blocks-item');
6240
6673
  if (!item || !list) return;
6241
6674
 
6675
+ if (action === 'toggle-block') {
6676
+ const content = item.querySelector('[data-block-content]');
6677
+ if (!(content instanceof HTMLElement)) return;
6678
+ setBlockExpanded(item, content.classList.contains('hidden'));
6679
+ syncBlocksState(container);
6680
+ return;
6681
+ }
6682
+
6242
6683
  if (action === 'remove-block') {
6243
6684
  if (typeof requestRepeaterDelete === 'function') {
6244
6685
  requestRepeaterDelete(() => {
6245
6686
  item.remove();
6246
6687
  updateHiddenInput();
6688
+ syncBlocksState(container);
6247
6689
  }, 'block');
6248
6690
  } else {
6249
6691
  item.remove();
6250
6692
  updateHiddenInput();
6693
+ syncBlocksState(container);
6251
6694
  }
6252
6695
  return;
6253
6696
  }
@@ -6257,6 +6700,7 @@ function getBlocksFieldScript() {
6257
6700
  if (previous) {
6258
6701
  list.insertBefore(item, previous);
6259
6702
  updateHiddenInput();
6703
+ syncBlocksState(container);
6260
6704
  }
6261
6705
  return;
6262
6706
  }
@@ -6266,6 +6710,7 @@ function getBlocksFieldScript() {
6266
6710
  if (next) {
6267
6711
  list.insertBefore(next, item);
6268
6712
  updateHiddenInput();
6713
+ syncBlocksState(container);
6269
6714
  }
6270
6715
  }
6271
6716
  });
@@ -6287,6 +6732,12 @@ function getBlocksFieldScript() {
6287
6732
  });
6288
6733
 
6289
6734
  updateHiddenInput();
6735
+ const savedBlocksState = readBlocksState(container);
6736
+ if (savedBlocksState) {
6737
+ applyBlocksState(container, savedBlocksState);
6738
+ } else {
6739
+ syncBlocksState(container);
6740
+ }
6290
6741
  });
6291
6742
  }
6292
6743
 
@@ -6323,6 +6774,7 @@ chunkLTKV7AE5_cjs.init_admin_layout_catalyst_template();
6323
6774
  function renderContentFormPage(data) {
6324
6775
  const isEdit = data.isEdit || !!data.id;
6325
6776
  const title = isEdit ? `Edit: ${data.title || "Content"}` : `New ${data.collection.display_name}`;
6777
+ const hasValidationErrors = Boolean(data.validationErrors && Object.keys(data.validationErrors).length > 0);
6326
6778
  const backUrl = data.referrerParams ? `/admin/content?${data.referrerParams}` : `/admin/content?collection=${data.collection.id}`;
6327
6779
  const coreFields = data.fields.filter((f) => ["title", "slug", "content"].includes(f.field_name));
6328
6780
  const contentFields = data.fields.filter((f) => !["title", "slug", "content"].includes(f.field_name) && !f.field_name.startsWith("meta_"));
@@ -6411,6 +6863,7 @@ function renderContentFormPage(data) {
6411
6863
  ${isEdit ? `hx-put="/admin/content/${data.id}"` : `hx-post="/admin/content"`}
6412
6864
  hx-target="#form-messages"
6413
6865
  hx-encoding="multipart/form-data"
6866
+ data-has-validation-errors="${hasValidationErrors ? "true" : "false"}"
6414
6867
  class="space-y-6"
6415
6868
  >
6416
6869
  <input type="hidden" name="collection_id" value="${data.collection.id}">
@@ -6691,39 +7144,456 @@ function renderContentFormPage(data) {
6691
7144
 
6692
7145
  <!-- Dynamic Field Scripts -->
6693
7146
  <script>
7147
+ const contentFormCollectionId = ${JSON.stringify(data.collection.id)};
7148
+
7149
+ function getFieldGroupScope() {
7150
+ const url = new URL(window.location.href);
7151
+ const urlCollectionId = url.searchParams.get('collection');
7152
+ const effectiveCollectionId = urlCollectionId || contentFormCollectionId || '';
7153
+ return window.location.pathname + ':' + effectiveCollectionId;
7154
+ }
7155
+
7156
+ function getItemPosition(itemSelector, item) {
7157
+ if (!(item instanceof Element)) return -1;
7158
+ const parent = item.parentElement;
7159
+ if (!parent) return -1;
7160
+ return Array.from(parent.querySelectorAll(':scope > ' + itemSelector)).indexOf(item);
7161
+ }
7162
+
7163
+ function stripIndexedFieldPrefix(fullFieldName, prefix) {
7164
+ if (!fullFieldName || !prefix || !fullFieldName.startsWith(prefix)) {
7165
+ return fullFieldName;
7166
+ }
7167
+
7168
+ const remainder = fullFieldName.slice(prefix.length);
7169
+ const indexMatch = remainder.match(/^(\\d+)(-|__)(.*)$/);
7170
+ if (!indexMatch) {
7171
+ return fullFieldName;
7172
+ }
7173
+
7174
+ return indexMatch[3];
7175
+ }
7176
+
7177
+ function getFieldGroupStorageKey(groupOrId) {
7178
+ const defaultGroupId = typeof groupOrId === 'string' ? groupOrId : (groupOrId?.getAttribute('data-group-id') || 'unknown');
7179
+ const group = typeof groupOrId === 'string'
7180
+ ? document.querySelector('.field-group[data-group-id="' + defaultGroupId + '"]')
7181
+ : groupOrId;
7182
+
7183
+ const scopePrefix = 'sonic:ui:objects:' + getFieldGroupScope() + ':';
7184
+ if (!(group instanceof Element)) {
7185
+ return scopePrefix + defaultGroupId;
7186
+ }
7187
+
7188
+ const fullFieldName = group.getAttribute('data-field-name') || '';
7189
+
7190
+ const blocksField = group.closest('.blocks-field');
7191
+ const blockItem = group.closest('.blocks-item');
7192
+ if (blocksField instanceof Element && blockItem instanceof Element) {
7193
+ const blocksFieldName = blocksField.getAttribute('data-field-name') || 'unknown';
7194
+ const blockPosition = getItemPosition('.blocks-item', blockItem);
7195
+ const relativePath = stripIndexedFieldPrefix(fullFieldName, 'block-' + blocksFieldName + '-') || defaultGroupId;
7196
+ return scopePrefix + 'blocks:' + blocksFieldName + ':' + blockPosition + ':' + relativePath;
7197
+ }
7198
+
7199
+ const arrayField = group.closest('[data-structured-array][data-field-name]');
7200
+ const arrayItem = group.closest('.structured-array-item');
7201
+ if (arrayField instanceof Element && arrayItem instanceof Element) {
7202
+ const arrayFieldName = arrayField.getAttribute('data-field-name') || 'unknown';
7203
+ const itemPosition = getItemPosition('.structured-array-item', arrayItem);
7204
+ const relativePath = stripIndexedFieldPrefix(fullFieldName, 'array-' + arrayFieldName + '-') || defaultGroupId;
7205
+ return scopePrefix + 'repeaters:' + arrayFieldName + ':' + itemPosition + ':' + relativePath;
7206
+ }
7207
+
7208
+ return scopePrefix + defaultGroupId;
7209
+ }
7210
+
7211
+ function loadFieldGroupState(group) {
7212
+ try {
7213
+ const value = sessionStorage.getItem(getFieldGroupStorageKey(group));
7214
+ if (value === '1') return true;
7215
+ if (value === '0') return false;
7216
+ } catch {}
7217
+ return null;
7218
+ }
7219
+
7220
+ function saveFieldGroupState(group, isCollapsed) {
7221
+ try {
7222
+ sessionStorage.setItem(getFieldGroupStorageKey(group), isCollapsed ? '1' : '0');
7223
+ } catch {}
7224
+ }
7225
+
7226
+ function resolveFieldGroupElements(groupOrId) {
7227
+ let group = null;
7228
+
7229
+ if (groupOrId instanceof Element) {
7230
+ group = groupOrId.classList.contains('field-group')
7231
+ ? groupOrId
7232
+ : groupOrId.closest('.field-group[data-group-id]');
7233
+ } else if (typeof groupOrId === 'string' && groupOrId) {
7234
+ group = document.querySelector('.field-group[data-group-id="' + groupOrId + '"]');
7235
+ }
7236
+
7237
+ let content = null;
7238
+ let icon = null;
7239
+
7240
+ if (group instanceof Element) {
7241
+ content = group.querySelector(':scope > .field-group-content');
7242
+ icon = group.querySelector(':scope > .field-group-header svg[id$="-icon"]');
7243
+ }
7244
+
7245
+ // Legacy fallback for any existing calls still passing string IDs.
7246
+ if (!(content instanceof HTMLElement) && typeof groupOrId === 'string') {
7247
+ content = document.getElementById(groupOrId + '-content');
7248
+ }
7249
+ if (!(icon instanceof Element) && typeof groupOrId === 'string') {
7250
+ icon = document.getElementById(groupOrId + '-icon');
7251
+ }
7252
+
7253
+ if (!(group instanceof Element) && content instanceof Element) {
7254
+ group = content.closest('.field-group[data-group-id]');
7255
+ }
7256
+
7257
+ return { group, content, icon };
7258
+ }
7259
+
7260
+ function applyFieldGroupState(groupOrId, isCollapsed) {
7261
+ const { content, icon } = resolveFieldGroupElements(groupOrId);
7262
+ if (!(content instanceof HTMLElement) || !(icon instanceof Element)) return;
7263
+ content.classList.toggle('hidden', isCollapsed);
7264
+ icon.classList.toggle('-rotate-90', isCollapsed);
7265
+ }
7266
+
7267
+ function restoreFieldGroupStates() {
7268
+ document.querySelectorAll('.field-group[data-group-id]').forEach((group) => {
7269
+ const savedState = loadFieldGroupState(group);
7270
+ if (savedState === null) return;
7271
+ applyFieldGroupState(group, savedState);
7272
+ });
7273
+ }
7274
+
7275
+ function persistAllFieldGroupStates() {
7276
+ document.querySelectorAll('.field-group[data-group-id]').forEach((group) => {
7277
+ const { content } = resolveFieldGroupElements(group);
7278
+ if (!(content instanceof HTMLElement)) return;
7279
+ saveFieldGroupState(group, content.classList.contains('hidden'));
7280
+ });
7281
+ }
7282
+
7283
+ function setValidationHeaderIndicator(container) {
7284
+ if (!(container instanceof Element)) return;
7285
+ let header = null;
7286
+ let markerTarget = null;
7287
+
7288
+ if (container.classList.contains('field-group')) {
7289
+ header = container.querySelector(':scope > .field-group-header');
7290
+ markerTarget = container.querySelector(':scope > .field-group-header h3');
7291
+ } else if (container.classList.contains('structured-array-item')) {
7292
+ header = container.querySelector('[data-action="toggle-item"]');
7293
+ markerTarget = header;
7294
+ } else if (container.classList.contains('blocks-item')) {
7295
+ header = container.querySelector('[data-action="toggle-block"]');
7296
+ markerTarget = header;
7297
+ }
7298
+
7299
+ if (!(header instanceof HTMLElement)) return;
7300
+ if (!(markerTarget instanceof HTMLElement)) {
7301
+ markerTarget = header;
7302
+ }
7303
+
7304
+ header.dataset.validationHeaderError = 'true';
7305
+ header.classList.add('text-pink-700', 'dark:text-pink-300');
7306
+
7307
+ if (!markerTarget.querySelector('[data-validation-indicator]')) {
7308
+ const marker = document.createElement('span');
7309
+ marker.setAttribute('data-validation-indicator', 'true');
7310
+ marker.className = 'ml-2 inline-block h-2 w-2 rounded-full bg-pink-500 align-middle';
7311
+ marker.setAttribute('aria-hidden', 'true');
7312
+ markerTarget.appendChild(marker);
7313
+ }
7314
+ }
7315
+
7316
+ function clearValidationIndicators() {
7317
+ document.querySelectorAll('[data-validation-header-error="true"]').forEach((el) => {
7318
+ if (!(el instanceof HTMLElement)) return;
7319
+ delete el.dataset.validationHeaderError;
7320
+ el.classList.remove('text-pink-700', 'dark:text-pink-300');
7321
+ });
7322
+
7323
+ document.querySelectorAll('[data-validation-indicator]').forEach((el) => el.remove());
7324
+ }
7325
+
7326
+ function expandContainerForValidation(container) {
7327
+ if (!(container instanceof Element)) return;
7328
+
7329
+ if (container.classList.contains('field-group')) {
7330
+ applyFieldGroupState(container, false);
7331
+ return;
7332
+ }
7333
+
7334
+ if (container.classList.contains('structured-array-item')) {
7335
+ const content = container.querySelector('[data-array-item-fields]');
7336
+ const icon = container.querySelector('[data-item-toggle-icon]');
7337
+ if (content instanceof HTMLElement) {
7338
+ content.classList.remove('hidden');
7339
+ }
7340
+ if (icon instanceof Element) {
7341
+ icon.classList.remove('-rotate-90');
7342
+ }
7343
+ return;
7344
+ }
7345
+
7346
+ if (container.classList.contains('blocks-item')) {
7347
+ const content = container.querySelector('[data-block-content]');
7348
+ const icon = container.querySelector('[data-block-toggle-icon]');
7349
+ if (content instanceof HTMLElement) {
7350
+ content.classList.remove('hidden');
7351
+ }
7352
+ if (icon instanceof Element) {
7353
+ icon.classList.remove('-rotate-90');
7354
+ }
7355
+ }
7356
+ }
7357
+
7358
+ function walkErrorContainers(node, expand) {
7359
+ if (!(node instanceof Element)) return;
7360
+ const visited = new Set();
7361
+ let cursor = node;
7362
+ while (cursor) {
7363
+ const candidates = [
7364
+ cursor.closest('.structured-array-item'),
7365
+ cursor.closest('.blocks-item'),
7366
+ cursor.closest('.field-group[data-group-id]')
7367
+ ].filter((c) => c instanceof Element && !visited.has(c));
7368
+
7369
+ if (candidates.length === 0) break;
7370
+
7371
+ // Pick nearest ancestor container to preserve "first-error path only".
7372
+ let nearest = candidates[0];
7373
+ let bestDistance = Number.MAX_SAFE_INTEGER;
7374
+ for (const candidate of candidates) {
7375
+ let distance = 0;
7376
+ let walker = cursor;
7377
+ while (walker && walker !== candidate) {
7378
+ walker = walker.parentElement;
7379
+ distance += 1;
7380
+ }
7381
+ if (distance < bestDistance) {
7382
+ bestDistance = distance;
7383
+ nearest = candidate;
7384
+ }
7385
+ }
7386
+
7387
+ visited.add(nearest);
7388
+ setValidationHeaderIndicator(nearest);
7389
+ if (expand) {
7390
+ expandContainerForValidation(nearest);
7391
+ }
7392
+ cursor = nearest.parentElement;
7393
+ }
7394
+ }
7395
+
7396
+ function getFocusableTargetFromErrorGroup(group) {
7397
+ if (!(group instanceof Element)) return null;
7398
+ return (
7399
+ group.querySelector('input:not([type="hidden"]):not([disabled]), textarea:not([disabled]), select:not([disabled]), [contenteditable="true"]') ||
7400
+ group.querySelector('button:not([disabled])')
7401
+ );
7402
+ }
7403
+
7404
+ function revealServerValidationErrors() {
7405
+ clearValidationIndicators();
7406
+
7407
+ const errorGroups = Array.from(document.querySelectorAll('.form-group[data-has-errors="true"]'));
7408
+ if (errorGroups.length === 0) return;
7409
+
7410
+ // Add indicators for all errored sections, expand only first-error path.
7411
+ errorGroups.forEach((group, index) => {
7412
+ walkErrorContainers(group, index === 0);
7413
+ });
7414
+
7415
+ const firstTarget = getFocusableTargetFromErrorGroup(errorGroups[0]);
7416
+ if (firstTarget instanceof HTMLElement) {
7417
+ firstTarget.scrollIntoView({ behavior: 'smooth', block: 'center' });
7418
+ firstTarget.focus({ preventScroll: true });
7419
+ }
7420
+ }
7421
+
7422
+ function revealNativeValidationErrors(form) {
7423
+ if (!(form instanceof HTMLFormElement)) return;
7424
+ clearValidationIndicators();
7425
+
7426
+ const invalidControls = Array.from(form.querySelectorAll(':invalid'));
7427
+ if (invalidControls.length === 0) return;
7428
+
7429
+ invalidControls.forEach((control, index) => {
7430
+ walkErrorContainers(control, index === 0);
7431
+ });
7432
+
7433
+ const first = invalidControls[0];
7434
+ if (first instanceof HTMLElement) {
7435
+ first.scrollIntoView({ behavior: 'smooth', block: 'center' });
7436
+ first.focus({ preventScroll: true });
7437
+ }
7438
+ }
7439
+
6694
7440
  // Field group toggle
6695
- function toggleFieldGroup(groupId) {
6696
- const content = document.getElementById(groupId + '-content');
6697
- const icon = document.getElementById(groupId + '-icon');
6698
-
6699
- if (content.classList.contains('hidden')) {
6700
- content.classList.remove('hidden');
6701
- icon.classList.remove('rotate-[-90deg]');
6702
- } else {
6703
- content.classList.add('hidden');
6704
- icon.classList.add('rotate-[-90deg]');
7441
+ function toggleFieldGroup(groupOrTrigger) {
7442
+ const { group, content } = resolveFieldGroupElements(groupOrTrigger);
7443
+ if (!(group instanceof Element)) return;
7444
+ if (!(content instanceof HTMLElement)) return;
7445
+
7446
+ const isCollapsed = !content.classList.contains('hidden');
7447
+ applyFieldGroupState(group, isCollapsed);
7448
+ saveFieldGroupState(group, isCollapsed);
7449
+ }
7450
+
7451
+ if (document.readyState === 'loading') {
7452
+ document.addEventListener('DOMContentLoaded', () => {
7453
+ restoreFieldGroupStates();
7454
+ const form = document.getElementById('content-form');
7455
+ if (form?.getAttribute('data-has-validation-errors') === 'true') {
7456
+ revealServerValidationErrors();
7457
+ }
7458
+ });
7459
+ } else {
7460
+ restoreFieldGroupStates();
7461
+ const form = document.getElementById('content-form');
7462
+ if (form?.getAttribute('data-has-validation-errors') === 'true') {
7463
+ revealServerValidationErrors();
6705
7464
  }
6706
7465
  }
6707
7466
 
7467
+ document.addEventListener('htmx:afterSwap', function() {
7468
+ setTimeout(() => {
7469
+ restoreFieldGroupStates();
7470
+ const form = document.getElementById('content-form');
7471
+ if (form?.getAttribute('data-has-validation-errors') === 'true') {
7472
+ revealServerValidationErrors();
7473
+ }
7474
+ }, 50);
7475
+ });
7476
+
7477
+ const contentFormEl = document.getElementById('content-form');
7478
+ if (contentFormEl instanceof HTMLFormElement) {
7479
+ contentFormEl.addEventListener('submit', () => {
7480
+ persistAllFieldGroupStates();
7481
+ }, true);
7482
+ }
7483
+
7484
+ window.addEventListener('beforeunload', () => {
7485
+ persistAllFieldGroupStates();
7486
+ });
7487
+
7488
+ document.addEventListener('visibilitychange', () => {
7489
+ if (document.visibilityState === 'hidden') {
7490
+ persistAllFieldGroupStates();
7491
+ }
7492
+ });
7493
+
7494
+ let pendingNativeValidationReveal = false;
7495
+ document.addEventListener('invalid', function(event) {
7496
+ const target = event.target;
7497
+ if (!(target instanceof Element)) return;
7498
+ const form = target.closest('form');
7499
+ if (!(form instanceof HTMLFormElement)) return;
7500
+
7501
+ if (pendingNativeValidationReveal) return;
7502
+ pendingNativeValidationReveal = true;
7503
+
7504
+ // Expand only first invalid path synchronously so the browser can focus it
7505
+ // and avoid "invalid form control is not focusable" errors.
7506
+ walkErrorContainers(target, true);
7507
+
7508
+ setTimeout(() => {
7509
+ pendingNativeValidationReveal = false;
7510
+ revealNativeValidationErrors(form);
7511
+ }, 0);
7512
+ }, true);
7513
+
6708
7514
  // Media field functions
6709
- let currentMediaFieldId = null;
7515
+ function notifyFieldChange(input) {
7516
+ if (!input) return;
7517
+ input.dispatchEvent(new Event('input', { bubbles: true }));
7518
+ input.dispatchEvent(new Event('change', { bubbles: true }));
7519
+ }
7520
+
7521
+ function getActiveMediaModal() {
7522
+ const modal = document.getElementById('media-selector-modal');
7523
+ return modal instanceof HTMLElement ? modal : null;
7524
+ }
7525
+
7526
+ function getMediaFieldElements(fieldId) {
7527
+ if (!fieldId) {
7528
+ return {
7529
+ fieldId: '',
7530
+ hiddenInput: null,
7531
+ preview: null,
7532
+ mediaField: null,
7533
+ actionsDiv: null,
7534
+ };
7535
+ }
7536
+
7537
+ const hiddenInput = document.getElementById(fieldId);
7538
+ const preview = document.getElementById(fieldId + '-preview');
7539
+ const mediaField = hiddenInput?.closest('.media-field-container') || null;
7540
+ const actionsDiv = mediaField?.querySelector('.media-actions') || null;
7541
+
7542
+ return {
7543
+ fieldId,
7544
+ hiddenInput,
7545
+ preview,
7546
+ mediaField,
7547
+ actionsDiv,
7548
+ };
7549
+ }
7550
+
7551
+ function getActiveMediaTarget() {
7552
+ const modal = getActiveMediaModal();
7553
+ const fieldId = modal?.dataset.targetFieldId || '';
7554
+ return {
7555
+ modal,
7556
+ originalValue: modal?.dataset.originalValue || '',
7557
+ ...getMediaFieldElements(fieldId),
7558
+ };
7559
+ }
7560
+
7561
+ function ensureSingleMediaRemoveButton(fieldId, actionsDiv) {
7562
+ if (!(actionsDiv instanceof HTMLElement)) return;
7563
+ const existingRemoveButton = actionsDiv.querySelector('[data-media-remove="true"]');
7564
+ if (existingRemoveButton) return;
7565
+
7566
+ const removeBtn = document.createElement('button');
7567
+ removeBtn.type = 'button';
7568
+ removeBtn.setAttribute('data-media-remove', 'true');
7569
+ removeBtn.onclick = () => clearMediaField(fieldId);
7570
+ removeBtn.className = 'inline-flex items-center px-4 py-2 bg-red-600 text-white rounded-xl hover:bg-red-700 transition-all';
7571
+ removeBtn.textContent = 'Remove';
7572
+ actionsDiv.appendChild(removeBtn);
7573
+ }
6710
7574
 
6711
7575
  function openMediaSelector(fieldId) {
6712
- currentMediaFieldId = fieldId;
7576
+ const existingModal = getActiveMediaModal();
7577
+ if (existingModal) {
7578
+ existingModal.remove();
7579
+ }
7580
+
6713
7581
  // Store the original value in case user cancels
6714
- const originalValue = document.getElementById(fieldId)?.value || '';
7582
+ const originalValue = getMediaFieldElements(fieldId).hiddenInput?.value || '';
6715
7583
 
6716
7584
  // Open media library modal
6717
7585
  const modal = document.createElement('div');
6718
7586
  modal.className = 'fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50';
6719
7587
  modal.id = 'media-selector-modal';
7588
+ modal.dataset.targetFieldId = fieldId;
7589
+ modal.dataset.originalValue = originalValue;
6720
7590
  modal.innerHTML = \`
6721
7591
  <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-4xl max-h-[90vh] overflow-y-auto">
6722
7592
  <h3 class="text-lg font-semibold text-zinc-950 dark:text-white mb-4">Select Media</h3>
6723
7593
  <div id="media-grid-container" hx-get="/admin/media/selector" hx-trigger="load"></div>
6724
7594
  <div class="mt-4 flex justify-end space-x-2">
6725
7595
  <button
6726
- onclick="cancelMediaSelection('\${fieldId}', '\${originalValue}')"
7596
+ onclick="cancelMediaSelection()"
6727
7597
  class="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 transition-colors">
6728
7598
  Cancel
6729
7599
  </button>
@@ -6743,23 +7613,23 @@ function renderContentFormPage(data) {
6743
7613
  }
6744
7614
 
6745
7615
  function closeMediaSelector() {
6746
- const modal = document.getElementById('media-selector-modal');
7616
+ const modal = getActiveMediaModal();
6747
7617
  if (modal) {
6748
7618
  modal.remove();
6749
7619
  }
6750
- currentMediaFieldId = null;
6751
7620
  }
6752
7621
 
6753
- function cancelMediaSelection(fieldId, originalValue) {
7622
+ function cancelMediaSelection() {
7623
+ const { hiddenInput, preview, originalValue } = getActiveMediaTarget();
7624
+
6754
7625
  // Restore original value
6755
- const hiddenInput = document.getElementById(fieldId);
6756
7626
  if (hiddenInput) {
6757
7627
  hiddenInput.value = originalValue;
7628
+ notifyFieldChange(hiddenInput);
6758
7629
  }
6759
7630
 
6760
7631
  // If original value was empty, hide the preview and show select button
6761
7632
  if (!originalValue) {
6762
- const preview = document.getElementById(fieldId + '-preview');
6763
7633
  if (preview) {
6764
7634
  preview.classList.add('hidden');
6765
7635
  }
@@ -6770,11 +7640,11 @@ function renderContentFormPage(data) {
6770
7640
  }
6771
7641
 
6772
7642
  function clearMediaField(fieldId) {
6773
- const hiddenInput = document.getElementById(fieldId);
6774
- const preview = document.getElementById(fieldId + '-preview');
7643
+ const { hiddenInput, preview, actionsDiv } = getMediaFieldElements(fieldId);
6775
7644
 
6776
7645
  if (hiddenInput) {
6777
7646
  hiddenInput.value = '';
7647
+ notifyFieldChange(hiddenInput);
6778
7648
  }
6779
7649
 
6780
7650
  if (preview) {
@@ -6784,25 +7654,34 @@ function renderContentFormPage(data) {
6784
7654
  }
6785
7655
  preview.classList.add('hidden');
6786
7656
  }
7657
+
7658
+ const removeButton = actionsDiv?.querySelector('[data-media-remove="true"]');
7659
+ if (removeButton) {
7660
+ removeButton.remove();
7661
+ }
6787
7662
  }
6788
7663
 
6789
7664
  // Global function to remove a single media from multiple selection
6790
7665
  window.removeMediaFromMultiple = function(fieldId, urlToRemove) {
6791
- const hiddenInput = document.getElementById(fieldId);
7666
+ const { hiddenInput, preview } = getMediaFieldElements(fieldId);
6792
7667
  if (!hiddenInput) return;
6793
7668
 
6794
7669
  const values = hiddenInput.value.split(',').filter(url => url !== urlToRemove);
6795
7670
  hiddenInput.value = values.join(',');
7671
+ notifyFieldChange(hiddenInput);
6796
7672
 
6797
7673
  // Remove preview item
6798
- const previewItem = document.querySelector(\`[data-url="\${urlToRemove}"]\`);
7674
+ const previewItem =
7675
+ preview &&
7676
+ Array.from(preview.querySelectorAll('[data-url]')).find(
7677
+ (item) => item.getAttribute('data-url') === urlToRemove,
7678
+ );
6799
7679
  if (previewItem) {
6800
7680
  previewItem.remove();
6801
7681
  }
6802
7682
 
6803
7683
  // Hide preview grid if empty
6804
7684
  if (values.length === 0) {
6805
- const preview = document.getElementById(fieldId + '-preview');
6806
7685
  if (preview) {
6807
7686
  preview.classList.add('hidden');
6808
7687
  }
@@ -6811,39 +7690,24 @@ function renderContentFormPage(data) {
6811
7690
 
6812
7691
  // Global function called by media selector buttons
6813
7692
  window.selectMediaFile = function(mediaId, mediaUrl, filename) {
6814
- if (!currentMediaFieldId) {
7693
+ const { fieldId, hiddenInput, preview, actionsDiv } = getActiveMediaTarget();
7694
+ if (!fieldId || !hiddenInput) {
6815
7695
  console.error('No field ID set for media selection');
6816
7696
  return;
6817
7697
  }
6818
7698
 
6819
- const fieldId = currentMediaFieldId;
6820
-
6821
7699
  // Set the hidden input value to the media URL (not ID)
6822
- const hiddenInput = document.getElementById(fieldId);
6823
- if (hiddenInput) {
6824
- hiddenInput.value = mediaUrl;
6825
- }
7700
+ hiddenInput.value = mediaUrl;
7701
+ notifyFieldChange(hiddenInput);
6826
7702
 
6827
7703
  // Update the preview
6828
- const preview = document.getElementById(fieldId + '-preview');
6829
7704
  if (preview) {
6830
7705
  preview.innerHTML = \`<img src="\${mediaUrl}" alt="\${filename}" class="w-32 h-32 object-cover rounded-lg border border-white/20">\`;
6831
7706
  preview.classList.remove('hidden');
6832
7707
  }
6833
7708
 
6834
7709
  // Show the remove button by finding the media actions container and updating it
6835
- const mediaField = hiddenInput?.closest('.media-field-container');
6836
- if (mediaField) {
6837
- const actionsDiv = mediaField.querySelector('.media-actions');
6838
- if (actionsDiv && !actionsDiv.querySelector('button:has-text("Remove")')) {
6839
- const removeBtn = document.createElement('button');
6840
- removeBtn.type = 'button';
6841
- removeBtn.onclick = () => clearMediaField(fieldId);
6842
- removeBtn.className = 'inline-flex items-center px-4 py-2 bg-red-600 text-white rounded-xl hover:bg-red-700 transition-all';
6843
- removeBtn.textContent = 'Remove';
6844
- actionsDiv.appendChild(removeBtn);
6845
- }
6846
- }
7710
+ ensureSingleMediaRemoveButton(fieldId, actionsDiv);
6847
7711
 
6848
7712
  // DON'T close the modal - let user click OK button
6849
7713
  // Visual feedback: highlight the selected item
@@ -6857,7 +7721,9 @@ function renderContentFormPage(data) {
6857
7721
  };
6858
7722
 
6859
7723
  function setMediaField(fieldId, mediaUrl) {
6860
- document.getElementById(fieldId).value = mediaUrl;
7724
+ const hiddenInput = document.getElementById(fieldId);
7725
+ hiddenInput.value = mediaUrl;
7726
+ notifyFieldChange(hiddenInput);
6861
7727
  const preview = document.getElementById(fieldId + '-preview');
6862
7728
  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">\`;
6863
7729
  preview.classList.remove('hidden');
@@ -8304,9 +9170,9 @@ function parseFieldValue(field, formData, options = {}) {
8304
9170
  const { skipValidation = false } = options;
8305
9171
  const value = formData.get(field.field_name);
8306
9172
  const errors = [];
8307
- const blocksConfig = chunkSHU7Q66Q_cjs.getBlocksFieldConfig(field.field_options);
9173
+ const blocksConfig = chunkE2GKK5HX_cjs.getBlocksFieldConfig(field.field_options);
8308
9174
  if (blocksConfig) {
8309
- const parsed = chunkSHU7Q66Q_cjs.parseBlocksValue(value, blocksConfig);
9175
+ const parsed = chunkE2GKK5HX_cjs.parseBlocksValue(value, blocksConfig);
8310
9176
  if (!skipValidation && field.is_required && parsed.value.length === 0) {
8311
9177
  parsed.errors.push(`${field.field_label} is required`);
8312
9178
  }
@@ -8416,9 +9282,9 @@ function extractFieldData(fields, formData, options = {}) {
8416
9282
  }
8417
9283
  return { data, errors };
8418
9284
  }
8419
- adminContentRoutes.use("*", chunkDQZVU3WB_cjs.requireAuth());
9285
+ adminContentRoutes.use("*", chunk5GO3AMON_cjs.requireAuth());
8420
9286
  async function getCollectionFields(db, collectionId) {
8421
- const cache = chunk64APW3DW_cjs.getCacheService(chunk64APW3DW_cjs.CACHE_CONFIGS.collection);
9287
+ const cache = chunkLFAQUR7P_cjs.getCacheService(chunkLFAQUR7P_cjs.CACHE_CONFIGS.collection);
8422
9288
  return cache.getOrSet(
8423
9289
  cache.generateKey("fields", collectionId),
8424
9290
  async () => {
@@ -8467,7 +9333,7 @@ async function getCollectionFields(db, collectionId) {
8467
9333
  );
8468
9334
  }
8469
9335
  async function getCollection(db, collectionId) {
8470
- const cache = chunk64APW3DW_cjs.getCacheService(chunk64APW3DW_cjs.CACHE_CONFIGS.collection);
9336
+ const cache = chunkLFAQUR7P_cjs.getCacheService(chunkLFAQUR7P_cjs.CACHE_CONFIGS.collection);
8471
9337
  return cache.getOrSet(
8472
9338
  cache.generateKey("collection", collectionId),
8473
9339
  async () => {
@@ -8495,7 +9361,7 @@ adminContentRoutes.get("/", async (c) => {
8495
9361
  const status = url.searchParams.get("status") || "all";
8496
9362
  const search = url.searchParams.get("search") || "";
8497
9363
  const offset = (page - 1) * limit;
8498
- const collectionsStmt = db.prepare("SELECT id, name, display_name FROM collections WHERE is_active = 1 ORDER BY display_name");
9364
+ const collectionsStmt = db.prepare("SELECT id, name, display_name FROM collections WHERE is_active = 1 AND (source_type IS NULL OR source_type = 'user') ORDER BY display_name");
8499
9365
  const { results: collectionsResults } = await collectionsStmt.all();
8500
9366
  const models = (collectionsResults || []).map((row) => ({
8501
9367
  name: row.name,
@@ -8503,6 +9369,7 @@ adminContentRoutes.get("/", async (c) => {
8503
9369
  }));
8504
9370
  const conditions = [];
8505
9371
  const params = [];
9372
+ conditions.push("(col.source_type IS NULL OR col.source_type = 'user')");
8506
9373
  if (status !== "deleted") {
8507
9374
  conditions.push("c.status != 'deleted'");
8508
9375
  }
@@ -8631,7 +9498,7 @@ adminContentRoutes.get("/new", async (c) => {
8631
9498
  const collectionId = url.searchParams.get("collection");
8632
9499
  if (!collectionId) {
8633
9500
  const db2 = c.env.DB;
8634
- const collectionsStmt = db2.prepare("SELECT id, name, display_name, description FROM collections WHERE is_active = 1 ORDER BY display_name");
9501
+ const collectionsStmt = db2.prepare("SELECT id, name, display_name, description FROM collections WHERE is_active = 1 AND (source_type IS NULL OR source_type = 'user') ORDER BY display_name");
8635
9502
  const { results } = await collectionsStmt.all();
8636
9503
  const collections = (results || []).map((row) => ({
8637
9504
  id: row.id,
@@ -8692,21 +9559,21 @@ adminContentRoutes.get("/new", async (c) => {
8692
9559
  const tinymceEnabled = await isPluginActive2(db, "tinymce-plugin");
8693
9560
  let tinymceSettings;
8694
9561
  if (tinymceEnabled) {
8695
- const pluginService = new chunkMPT5PA6U_cjs.PluginService(db);
9562
+ const pluginService = new chunkTWCQVJ6M_cjs.PluginService(db);
8696
9563
  const tinymcePlugin2 = await pluginService.getPlugin("tinymce-plugin");
8697
9564
  tinymceSettings = tinymcePlugin2?.settings;
8698
9565
  }
8699
9566
  const quillEnabled = await isPluginActive2(db, "quill-editor");
8700
9567
  let quillSettings;
8701
9568
  if (quillEnabled) {
8702
- const pluginService = new chunkMPT5PA6U_cjs.PluginService(db);
9569
+ const pluginService = new chunkTWCQVJ6M_cjs.PluginService(db);
8703
9570
  const quillPlugin = await pluginService.getPlugin("quill-editor");
8704
9571
  quillSettings = quillPlugin?.settings;
8705
9572
  }
8706
9573
  const mdxeditorEnabled = await isPluginActive2(db, "easy-mdx");
8707
9574
  let mdxeditorSettings;
8708
9575
  if (mdxeditorEnabled) {
8709
- const pluginService = new chunkMPT5PA6U_cjs.PluginService(db);
9576
+ const pluginService = new chunkTWCQVJ6M_cjs.PluginService(db);
8710
9577
  const mdxeditorPlugin = await pluginService.getPlugin("easy-mdx");
8711
9578
  mdxeditorSettings = mdxeditorPlugin?.settings;
8712
9579
  }
@@ -8756,7 +9623,7 @@ adminContentRoutes.get("/:id/edit", async (c) => {
8756
9623
  const db = c.env.DB;
8757
9624
  const url = new URL(c.req.url);
8758
9625
  const referrerParams = url.searchParams.get("ref") || "";
8759
- const cache = chunk64APW3DW_cjs.getCacheService(chunk64APW3DW_cjs.CACHE_CONFIGS.content);
9626
+ const cache = chunkLFAQUR7P_cjs.getCacheService(chunkLFAQUR7P_cjs.CACHE_CONFIGS.content);
8760
9627
  const content = await cache.getOrSet(
8761
9628
  cache.generateKey("content", id),
8762
9629
  async () => {
@@ -8797,21 +9664,21 @@ adminContentRoutes.get("/:id/edit", async (c) => {
8797
9664
  const tinymceEnabled = await isPluginActive2(db, "tinymce-plugin");
8798
9665
  let tinymceSettings;
8799
9666
  if (tinymceEnabled) {
8800
- const pluginService = new chunkMPT5PA6U_cjs.PluginService(db);
9667
+ const pluginService = new chunkTWCQVJ6M_cjs.PluginService(db);
8801
9668
  const tinymcePlugin2 = await pluginService.getPlugin("tinymce-plugin");
8802
9669
  tinymceSettings = tinymcePlugin2?.settings;
8803
9670
  }
8804
9671
  const quillEnabled = await isPluginActive2(db, "quill-editor");
8805
9672
  let quillSettings;
8806
9673
  if (quillEnabled) {
8807
- const pluginService = new chunkMPT5PA6U_cjs.PluginService(db);
9674
+ const pluginService = new chunkTWCQVJ6M_cjs.PluginService(db);
8808
9675
  const quillPlugin = await pluginService.getPlugin("quill-editor");
8809
9676
  quillSettings = quillPlugin?.settings;
8810
9677
  }
8811
9678
  const mdxeditorEnabled = await isPluginActive2(db, "easy-mdx");
8812
9679
  let mdxeditorSettings;
8813
9680
  if (mdxeditorEnabled) {
8814
- const pluginService = new chunkMPT5PA6U_cjs.PluginService(db);
9681
+ const pluginService = new chunkTWCQVJ6M_cjs.PluginService(db);
8815
9682
  const mdxeditorPlugin = await pluginService.getPlugin("easy-mdx");
8816
9683
  mdxeditorSettings = mdxeditorPlugin?.settings;
8817
9684
  }
@@ -8932,7 +9799,7 @@ adminContentRoutes.post("/", async (c) => {
8932
9799
  now,
8933
9800
  now
8934
9801
  ).run();
8935
- const cache = chunk64APW3DW_cjs.getCacheService(chunk64APW3DW_cjs.CACHE_CONFIGS.content);
9802
+ const cache = chunkLFAQUR7P_cjs.getCacheService(chunkLFAQUR7P_cjs.CACHE_CONFIGS.content);
8936
9803
  await cache.invalidate(`content:list:${collectionId}:*`);
8937
9804
  const versionStmt = db.prepare(`
8938
9805
  INSERT INTO content_versions (id, content_id, version, data, author_id, created_at)
@@ -9051,7 +9918,7 @@ adminContentRoutes.put("/:id", async (c) => {
9051
9918
  now,
9052
9919
  id
9053
9920
  ).run();
9054
- const cache = chunk64APW3DW_cjs.getCacheService(chunk64APW3DW_cjs.CACHE_CONFIGS.content);
9921
+ const cache = chunkLFAQUR7P_cjs.getCacheService(chunkLFAQUR7P_cjs.CACHE_CONFIGS.content);
9055
9922
  await cache.delete(cache.generateKey("content", id));
9056
9923
  await cache.invalidate(`content:list:${existingContent.collection_id}:*`);
9057
9924
  const existingData = JSON.parse(existingContent.data || "{}");
@@ -9106,7 +9973,7 @@ adminContentRoutes.put("/:id", async (c) => {
9106
9973
  `);
9107
9974
  }
9108
9975
  });
9109
- adminContentRoutes.post("/preview", chunkDQZVU3WB_cjs.requireRole(["admin", "editor", "author"]), async (c) => {
9976
+ adminContentRoutes.post("/preview", chunk5GO3AMON_cjs.requireRole(["admin", "editor", "author"]), async (c) => {
9110
9977
  try {
9111
9978
  const formData = await c.req.formData();
9112
9979
  const collectionId = formData.get("collection_id");
@@ -9328,7 +10195,7 @@ adminContentRoutes.post("/bulk-action", async (c) => {
9328
10195
  } else {
9329
10196
  return c.json({ success: false, error: "Invalid action" });
9330
10197
  }
9331
- const cache = chunk64APW3DW_cjs.getCacheService(chunk64APW3DW_cjs.CACHE_CONFIGS.content);
10198
+ const cache = chunkLFAQUR7P_cjs.getCacheService(chunkLFAQUR7P_cjs.CACHE_CONFIGS.content);
9332
10199
  for (const contentId of ids) {
9333
10200
  await cache.delete(cache.generateKey("content", contentId));
9334
10201
  }
@@ -9356,7 +10223,7 @@ adminContentRoutes.delete("/:id", async (c) => {
9356
10223
  WHERE id = ?
9357
10224
  `);
9358
10225
  await deleteStmt.bind(now, id).run();
9359
- const cache = chunk64APW3DW_cjs.getCacheService(chunk64APW3DW_cjs.CACHE_CONFIGS.content);
10226
+ const cache = chunkLFAQUR7P_cjs.getCacheService(chunkLFAQUR7P_cjs.CACHE_CONFIGS.content);
9360
10227
  await cache.delete(cache.generateKey("content", id));
9361
10228
  await cache.invalidate("content:list:*");
9362
10229
  return c.html(`
@@ -9484,7 +10351,7 @@ adminContentRoutes.post("/:id/restore/:version", async (c) => {
9484
10351
  return c.json({ success: false, error: "Failed to restore version" });
9485
10352
  }
9486
10353
  });
9487
- adminContentRoutes.get("/:id/version/:version/preview", chunkDQZVU3WB_cjs.requireRole(["admin", "editor", "author"]), async (c) => {
10354
+ adminContentRoutes.get("/:id/version/:version/preview", chunk5GO3AMON_cjs.requireRole(["admin", "editor", "author"]), async (c) => {
9488
10355
  try {
9489
10356
  const id = c.req.param("id");
9490
10357
  const version = parseInt(c.req.param("version") || "0");
@@ -11451,7 +12318,14 @@ function renderUsersListPage(data) {
11451
12318
 
11452
12319
  // src/routes/admin-users.ts
11453
12320
  var userRoutes = new hono.Hono();
11454
- userRoutes.use("*", chunkDQZVU3WB_cjs.requireAuth());
12321
+ userRoutes.use("*", chunk5GO3AMON_cjs.requireAuth());
12322
+ userRoutes.use("/users/*", chunk5GO3AMON_cjs.requireRole(["admin"]));
12323
+ userRoutes.use("/users", chunk5GO3AMON_cjs.requireRole(["admin"]));
12324
+ userRoutes.use("/invite-user", chunk5GO3AMON_cjs.requireRole(["admin"]));
12325
+ userRoutes.use("/resend-invitation/*", chunk5GO3AMON_cjs.requireRole(["admin"]));
12326
+ userRoutes.use("/cancel-invitation/*", chunk5GO3AMON_cjs.requireRole(["admin"]));
12327
+ userRoutes.use("/activity-logs", chunk5GO3AMON_cjs.requireRole(["admin"]));
12328
+ userRoutes.use("/activity-logs/*", chunk5GO3AMON_cjs.requireRole(["admin"]));
11455
12329
  userRoutes.get("/", (c) => {
11456
12330
  return c.redirect("/admin/dashboard");
11457
12331
  });
@@ -11606,7 +12480,7 @@ userRoutes.put("/profile", async (c) => {
11606
12480
  Date.now(),
11607
12481
  user.userId
11608
12482
  ).run();
11609
- await chunkDQZVU3WB_cjs.logActivity(
12483
+ await chunk5GO3AMON_cjs.logActivity(
11610
12484
  db,
11611
12485
  user.userId,
11612
12486
  "profile.update",
@@ -11669,7 +12543,7 @@ userRoutes.post("/profile/avatar", async (c) => {
11669
12543
  SELECT first_name, last_name FROM users WHERE id = ?
11670
12544
  `);
11671
12545
  const userData = await userStmt.bind(user.userId).first();
11672
- await chunkDQZVU3WB_cjs.logActivity(
12546
+ await chunk5GO3AMON_cjs.logActivity(
11673
12547
  db,
11674
12548
  user.userId,
11675
12549
  "profile.avatar_update",
@@ -11740,7 +12614,7 @@ userRoutes.post("/profile/password", async (c) => {
11740
12614
  dismissible: true
11741
12615
  }));
11742
12616
  }
11743
- const validPassword = await chunkDQZVU3WB_cjs.AuthManager.verifyPassword(currentPassword, userData.password_hash);
12617
+ const validPassword = await chunk5GO3AMON_cjs.AuthManager.verifyPassword(currentPassword, userData.password_hash);
11744
12618
  if (!validPassword) {
11745
12619
  return c.html(renderAlert2({
11746
12620
  type: "error",
@@ -11748,7 +12622,7 @@ userRoutes.post("/profile/password", async (c) => {
11748
12622
  dismissible: true
11749
12623
  }));
11750
12624
  }
11751
- const newPasswordHash = await chunkDQZVU3WB_cjs.AuthManager.hashPassword(newPassword);
12625
+ const newPasswordHash = await chunk5GO3AMON_cjs.AuthManager.hashPassword(newPassword);
11752
12626
  const historyStmt = db.prepare(`
11753
12627
  INSERT INTO password_history (id, user_id, password_hash, created_at)
11754
12628
  VALUES (?, ?, ?, ?)
@@ -11764,7 +12638,7 @@ userRoutes.post("/profile/password", async (c) => {
11764
12638
  WHERE id = ?
11765
12639
  `);
11766
12640
  await updateStmt.bind(newPasswordHash, Date.now(), user.userId).run();
11767
- await chunkDQZVU3WB_cjs.logActivity(
12641
+ await chunk5GO3AMON_cjs.logActivity(
11768
12642
  db,
11769
12643
  user.userId,
11770
12644
  "profile.password_change",
@@ -11831,7 +12705,7 @@ userRoutes.get("/users", async (c) => {
11831
12705
  `);
11832
12706
  const countResult = await countStmt.bind(...params).first();
11833
12707
  const totalUsers = countResult?.total || 0;
11834
- await chunkDQZVU3WB_cjs.logActivity(
12708
+ await chunk5GO3AMON_cjs.logActivity(
11835
12709
  db,
11836
12710
  user.userId,
11837
12711
  "users.list_view",
@@ -11939,7 +12813,9 @@ userRoutes.post("/users/new", async (c) => {
11939
12813
  const email = formData.get("email")?.toString()?.trim().toLowerCase() || "";
11940
12814
  const phone = chunkMNWKYY5E_cjs.sanitizeInput(formData.get("phone")?.toString()) || null;
11941
12815
  const bio = chunkMNWKYY5E_cjs.sanitizeInput(formData.get("bio")?.toString()) || null;
11942
- const role = formData.get("role")?.toString() || "viewer";
12816
+ const roleInput = formData.get("role")?.toString() || "viewer";
12817
+ const validRoles = ["admin", "editor", "author", "viewer"];
12818
+ const role = validRoles.includes(roleInput) ? roleInput : "viewer";
11943
12819
  const password = formData.get("password")?.toString() || "";
11944
12820
  const confirmPassword = formData.get("confirm_password")?.toString() || "";
11945
12821
  const isActive = formData.get("is_active") === "1";
@@ -11985,7 +12861,7 @@ userRoutes.post("/users/new", async (c) => {
11985
12861
  dismissible: true
11986
12862
  }));
11987
12863
  }
11988
- const passwordHash = await chunkDQZVU3WB_cjs.AuthManager.hashPassword(password);
12864
+ const passwordHash = await chunk5GO3AMON_cjs.AuthManager.hashPassword(password);
11989
12865
  const userId = crypto.randomUUID();
11990
12866
  const createStmt = db.prepare(`
11991
12867
  INSERT INTO users (
@@ -12008,7 +12884,7 @@ userRoutes.post("/users/new", async (c) => {
12008
12884
  Date.now(),
12009
12885
  Date.now()
12010
12886
  ).run();
12011
- await chunkDQZVU3WB_cjs.logActivity(
12887
+ await chunk5GO3AMON_cjs.logActivity(
12012
12888
  db,
12013
12889
  user.userId,
12014
12890
  "user!.create",
@@ -12046,7 +12922,7 @@ userRoutes.get("/users/:id", async (c) => {
12046
12922
  if (!userRecord) {
12047
12923
  return c.json({ error: "User not found" }, 404);
12048
12924
  }
12049
- await chunkDQZVU3WB_cjs.logActivity(
12925
+ await chunk5GO3AMON_cjs.logActivity(
12050
12926
  db,
12051
12927
  user.userId,
12052
12928
  "user!.view",
@@ -12159,7 +13035,9 @@ userRoutes.put("/users/:id", async (c) => {
12159
13035
  const username = chunkMNWKYY5E_cjs.sanitizeInput(formData.get("username")?.toString());
12160
13036
  const email = formData.get("email")?.toString()?.trim().toLowerCase() || "";
12161
13037
  const phone = chunkMNWKYY5E_cjs.sanitizeInput(formData.get("phone")?.toString()) || null;
12162
- const role = formData.get("role")?.toString() || "viewer";
13038
+ const roleInput = formData.get("role")?.toString() || "viewer";
13039
+ const validRoles = ["admin", "editor", "author", "viewer"];
13040
+ const role = validRoles.includes(roleInput) ? roleInput : "viewer";
12163
13041
  const isActive = formData.get("is_active") === "1";
12164
13042
  const emailVerified = formData.get("email_verified") === "1";
12165
13043
  const profileDisplayName = chunkMNWKYY5E_cjs.sanitizeInput(formData.get("profile_display_name")?.toString()) || null;
@@ -12271,7 +13149,7 @@ userRoutes.put("/users/:id", async (c) => {
12271
13149
  ).run();
12272
13150
  }
12273
13151
  }
12274
- await chunkDQZVU3WB_cjs.logActivity(
13152
+ await chunk5GO3AMON_cjs.logActivity(
12275
13153
  db,
12276
13154
  user.userId,
12277
13155
  "user.update",
@@ -12316,7 +13194,7 @@ userRoutes.post("/users/:id/toggle", async (c) => {
12316
13194
  UPDATE users SET is_active = ?, updated_at = ? WHERE id = ?
12317
13195
  `);
12318
13196
  await toggleStmt.bind(active ? 1 : 0, Date.now(), userId).run();
12319
- await chunkDQZVU3WB_cjs.logActivity(
13197
+ await chunk5GO3AMON_cjs.logActivity(
12320
13198
  db,
12321
13199
  user.userId,
12322
13200
  active ? "user.activate" : "user.deactivate",
@@ -12357,7 +13235,7 @@ userRoutes.delete("/users/:id", async (c) => {
12357
13235
  DELETE FROM users WHERE id = ?
12358
13236
  `);
12359
13237
  await deleteStmt.bind(userId).run();
12360
- await chunkDQZVU3WB_cjs.logActivity(
13238
+ await chunk5GO3AMON_cjs.logActivity(
12361
13239
  db,
12362
13240
  user.userId,
12363
13241
  "user!.hard_delete",
@@ -12376,7 +13254,7 @@ userRoutes.delete("/users/:id", async (c) => {
12376
13254
  UPDATE users SET is_active = 0, updated_at = ? WHERE id = ?
12377
13255
  `);
12378
13256
  await deleteStmt.bind(Date.now(), userId).run();
12379
- await chunkDQZVU3WB_cjs.logActivity(
13257
+ await chunk5GO3AMON_cjs.logActivity(
12380
13258
  db,
12381
13259
  user.userId,
12382
13260
  "user!.soft_delete",
@@ -12442,7 +13320,7 @@ userRoutes.post("/invite-user", async (c) => {
12442
13320
  Date.now(),
12443
13321
  Date.now()
12444
13322
  ).run();
12445
- await chunkDQZVU3WB_cjs.logActivity(
13323
+ await chunk5GO3AMON_cjs.logActivity(
12446
13324
  db,
12447
13325
  user.userId,
12448
13326
  "user!.invite_sent",
@@ -12499,7 +13377,7 @@ userRoutes.post("/resend-invitation/:id", async (c) => {
12499
13377
  Date.now(),
12500
13378
  userId
12501
13379
  ).run();
12502
- await chunkDQZVU3WB_cjs.logActivity(
13380
+ await chunk5GO3AMON_cjs.logActivity(
12503
13381
  db,
12504
13382
  user.userId,
12505
13383
  "user!.invitation_resent",
@@ -12535,7 +13413,7 @@ userRoutes.delete("/cancel-invitation/:id", async (c) => {
12535
13413
  }
12536
13414
  const deleteStmt = db.prepare(`DELETE FROM users WHERE id = ?`);
12537
13415
  await deleteStmt.bind(userId).run();
12538
- await chunkDQZVU3WB_cjs.logActivity(
13416
+ await chunk5GO3AMON_cjs.logActivity(
12539
13417
  db,
12540
13418
  user.userId,
12541
13419
  "user!.invitation_cancelled",
@@ -12618,7 +13496,7 @@ userRoutes.get("/activity-logs", async (c) => {
12618
13496
  ...log,
12619
13497
  details: log.details ? JSON.parse(log.details) : null
12620
13498
  }));
12621
- await chunkDQZVU3WB_cjs.logActivity(
13499
+ await chunk5GO3AMON_cjs.logActivity(
12622
13500
  db,
12623
13501
  user.userId,
12624
13502
  "activity.logs_viewed",
@@ -12725,7 +13603,7 @@ userRoutes.get("/activity-logs/export", async (c) => {
12725
13603
  csvRows.push(row.join(","));
12726
13604
  }
12727
13605
  const csvContent = csvRows.join("\n");
12728
- await chunkDQZVU3WB_cjs.logActivity(
13606
+ await chunk5GO3AMON_cjs.logActivity(
12729
13607
  db,
12730
13608
  user.userId,
12731
13609
  "activity.logs_exported",
@@ -14064,7 +14942,7 @@ var fileValidationSchema2 = zod.z.object({
14064
14942
  // 50MB max
14065
14943
  });
14066
14944
  var adminMediaRoutes = new hono.Hono();
14067
- adminMediaRoutes.use("*", chunkDQZVU3WB_cjs.requireAuth());
14945
+ adminMediaRoutes.use("*", chunk5GO3AMON_cjs.requireAuth());
14068
14946
  adminMediaRoutes.get("/", async (c) => {
14069
14947
  try {
14070
14948
  const user = c.get("user");
@@ -14650,7 +15528,7 @@ adminMediaRoutes.put("/:id", async (c) => {
14650
15528
  `);
14651
15529
  }
14652
15530
  });
14653
- adminMediaRoutes.delete("/cleanup", chunkDQZVU3WB_cjs.requireRole("admin"), async (c) => {
15531
+ adminMediaRoutes.delete("/cleanup", chunk5GO3AMON_cjs.requireRole("admin"), async (c) => {
14654
15532
  try {
14655
15533
  const db = c.env.DB;
14656
15534
  const allMediaStmt = db.prepare("SELECT id, r2_key, filename FROM media WHERE deleted_at IS NULL");
@@ -16873,7 +17751,7 @@ function renderEmailSettingsContent(plugin, settings) {
16873
17751
 
16874
17752
  // src/routes/admin-plugins.ts
16875
17753
  var adminPluginRoutes = new hono.Hono();
16876
- adminPluginRoutes.use("*", chunkDQZVU3WB_cjs.requireAuth());
17754
+ adminPluginRoutes.use("*", chunk5GO3AMON_cjs.requireAuth());
16877
17755
  var AVAILABLE_PLUGINS = [
16878
17756
  {
16879
17757
  id: "third-party-faq",
@@ -17000,7 +17878,7 @@ adminPluginRoutes.get("/", async (c) => {
17000
17878
  if (user?.role !== "admin") {
17001
17879
  return c.text("Access denied", 403);
17002
17880
  }
17003
- const pluginService = new chunkMPT5PA6U_cjs.PluginService(db);
17881
+ const pluginService = new chunkTWCQVJ6M_cjs.PluginService(db);
17004
17882
  let installedPlugins = [];
17005
17883
  let stats = { total: 0, active: 0, inactive: 0, errors: 0, uninstalled: 0 };
17006
17884
  try {
@@ -17076,7 +17954,7 @@ adminPluginRoutes.get("/:id", async (c) => {
17076
17954
  if (user?.role !== "admin") {
17077
17955
  return c.redirect("/admin/plugins");
17078
17956
  }
17079
- const pluginService = new chunkMPT5PA6U_cjs.PluginService(db);
17957
+ const pluginService = new chunkTWCQVJ6M_cjs.PluginService(db);
17080
17958
  const plugin = await pluginService.getPlugin(pluginId);
17081
17959
  if (!plugin) {
17082
17960
  return c.text("Plugin not found", 404);
@@ -17160,7 +18038,7 @@ adminPluginRoutes.post("/:id/activate", async (c) => {
17160
18038
  if (user?.role !== "admin") {
17161
18039
  return c.json({ error: "Access denied" }, 403);
17162
18040
  }
17163
- const pluginService = new chunkMPT5PA6U_cjs.PluginService(db);
18041
+ const pluginService = new chunkTWCQVJ6M_cjs.PluginService(db);
17164
18042
  await pluginService.activatePlugin(pluginId);
17165
18043
  return c.json({ success: true });
17166
18044
  } catch (error) {
@@ -17177,7 +18055,7 @@ adminPluginRoutes.post("/:id/deactivate", async (c) => {
17177
18055
  if (user?.role !== "admin") {
17178
18056
  return c.json({ error: "Access denied" }, 403);
17179
18057
  }
17180
- const pluginService = new chunkMPT5PA6U_cjs.PluginService(db);
18058
+ const pluginService = new chunkTWCQVJ6M_cjs.PluginService(db);
17181
18059
  await pluginService.deactivatePlugin(pluginId);
17182
18060
  return c.json({ success: true });
17183
18061
  } catch (error) {
@@ -17194,7 +18072,7 @@ adminPluginRoutes.post("/install", async (c) => {
17194
18072
  return c.json({ error: "Access denied" }, 403);
17195
18073
  }
17196
18074
  const body = await c.req.json();
17197
- const pluginService = new chunkMPT5PA6U_cjs.PluginService(db);
18075
+ const pluginService = new chunkTWCQVJ6M_cjs.PluginService(db);
17198
18076
  if (body.name === "faq-plugin") {
17199
18077
  const faqPlugin = await pluginService.installPlugin({
17200
18078
  id: "third-party-faq",
@@ -17464,7 +18342,7 @@ adminPluginRoutes.post("/:id/uninstall", async (c) => {
17464
18342
  if (user?.role !== "admin") {
17465
18343
  return c.json({ error: "Access denied" }, 403);
17466
18344
  }
17467
- const pluginService = new chunkMPT5PA6U_cjs.PluginService(db);
18345
+ const pluginService = new chunkTWCQVJ6M_cjs.PluginService(db);
17468
18346
  await pluginService.uninstallPlugin(pluginId);
17469
18347
  return c.json({ success: true });
17470
18348
  } catch (error) {
@@ -17482,8 +18360,20 @@ adminPluginRoutes.post("/:id/settings", async (c) => {
17482
18360
  return c.json({ error: "Access denied" }, 403);
17483
18361
  }
17484
18362
  const settings = await c.req.json();
17485
- const pluginService = new chunkMPT5PA6U_cjs.PluginService(db);
18363
+ const pluginService = new chunkTWCQVJ6M_cjs.PluginService(db);
17486
18364
  await pluginService.updatePluginSettings(pluginId, settings);
18365
+ if (pluginId === "core-auth") {
18366
+ try {
18367
+ const cacheKv = c.env.CACHE_KV;
18368
+ if (cacheKv) {
18369
+ await cacheKv.delete("auth:settings");
18370
+ await cacheKv.delete("auth:registration-enabled");
18371
+ console.log("[AuthSettings] Cache cleared after updating authentication settings");
18372
+ }
18373
+ } catch (cacheError) {
18374
+ console.error("[AuthSettings] Failed to clear cache:", cacheError);
18375
+ }
18376
+ }
17487
18377
  return c.json({ success: true });
17488
18378
  } catch (error) {
17489
18379
  console.error("Error updating plugin settings:", error);
@@ -18278,11 +19168,11 @@ function renderLogConfigPage(data) {
18278
19168
 
18279
19169
  // src/routes/admin-logs.ts
18280
19170
  var adminLogsRoutes = new hono.Hono();
18281
- adminLogsRoutes.use("*", chunkDQZVU3WB_cjs.requireAuth());
19171
+ adminLogsRoutes.use("*", chunk5GO3AMON_cjs.requireAuth());
18282
19172
  adminLogsRoutes.get("/", async (c) => {
18283
19173
  try {
18284
19174
  const user = c.get("user");
18285
- const logger = chunk64APW3DW_cjs.getLogger(c.env.DB);
19175
+ const logger = chunkLFAQUR7P_cjs.getLogger(c.env.DB);
18286
19176
  const query = c.req.query();
18287
19177
  const page = parseInt(query.page || "1");
18288
19178
  const limit = parseInt(query.limit || "50");
@@ -18362,7 +19252,7 @@ adminLogsRoutes.get("/:id", async (c) => {
18362
19252
  try {
18363
19253
  const id = c.req.param("id");
18364
19254
  const user = c.get("user");
18365
- const logger = chunk64APW3DW_cjs.getLogger(c.env.DB);
19255
+ const logger = chunkLFAQUR7P_cjs.getLogger(c.env.DB);
18366
19256
  const { logs } = await logger.getLogs({
18367
19257
  limit: 1,
18368
19258
  offset: 0,
@@ -18399,7 +19289,7 @@ adminLogsRoutes.get("/:id", async (c) => {
18399
19289
  adminLogsRoutes.get("/config", async (c) => {
18400
19290
  try {
18401
19291
  const user = c.get("user");
18402
- const logger = chunk64APW3DW_cjs.getLogger(c.env.DB);
19292
+ const logger = chunkLFAQUR7P_cjs.getLogger(c.env.DB);
18403
19293
  const configs = await logger.getAllConfigs();
18404
19294
  const pageData = {
18405
19295
  configs,
@@ -18423,7 +19313,7 @@ adminLogsRoutes.post("/config/:category", async (c) => {
18423
19313
  const level = formData.get("level");
18424
19314
  const retention = parseInt(formData.get("retention"));
18425
19315
  const maxSize = parseInt(formData.get("max_size"));
18426
- const logger = chunk64APW3DW_cjs.getLogger(c.env.DB);
19316
+ const logger = chunkLFAQUR7P_cjs.getLogger(c.env.DB);
18427
19317
  await logger.updateConfig(category, {
18428
19318
  enabled,
18429
19319
  level,
@@ -18452,7 +19342,7 @@ adminLogsRoutes.get("/export", async (c) => {
18452
19342
  const category = query.category;
18453
19343
  const startDate = query.start_date;
18454
19344
  const endDate = query.end_date;
18455
- const logger = chunk64APW3DW_cjs.getLogger(c.env.DB);
19345
+ const logger = chunkLFAQUR7P_cjs.getLogger(c.env.DB);
18456
19346
  const filter = {
18457
19347
  limit: 1e4,
18458
19348
  // Export up to 10k logs
@@ -18533,7 +19423,7 @@ adminLogsRoutes.post("/cleanup", async (c) => {
18533
19423
  error: "Unauthorized. Admin access required."
18534
19424
  }, 403);
18535
19425
  }
18536
- const logger = chunk64APW3DW_cjs.getLogger(c.env.DB);
19426
+ const logger = chunkLFAQUR7P_cjs.getLogger(c.env.DB);
18537
19427
  await logger.cleanupByRetention();
18538
19428
  return c.html(html.html`
18539
19429
  <div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded">
@@ -18555,7 +19445,7 @@ adminLogsRoutes.post("/search", async (c) => {
18555
19445
  const search = formData.get("search");
18556
19446
  const level = formData.get("level");
18557
19447
  const category = formData.get("category");
18558
- const logger = chunk64APW3DW_cjs.getLogger(c.env.DB);
19448
+ const logger = chunkLFAQUR7P_cjs.getLogger(c.env.DB);
18559
19449
  const filter = {
18560
19450
  limit: 20,
18561
19451
  offset: 0,
@@ -20606,9 +21496,9 @@ function renderStorageUsage(databaseSizeBytes, mediaSizeBytes) {
20606
21496
  }
20607
21497
 
20608
21498
  // src/routes/admin-dashboard.ts
20609
- var VERSION = chunkSHU7Q66Q_cjs.getCoreVersion();
21499
+ var VERSION = chunkE2GKK5HX_cjs.getCoreVersion();
20610
21500
  var router = new hono.Hono();
20611
- router.use("*", chunkDQZVU3WB_cjs.requireAuth());
21501
+ router.use("*", chunk5GO3AMON_cjs.requireAuth());
20612
21502
  router.get("/", async (c) => {
20613
21503
  const user = c.get("user");
20614
21504
  try {
@@ -20639,7 +21529,7 @@ router.get("/stats", async (c) => {
20639
21529
  const db = c.env.DB;
20640
21530
  let collectionsCount = 0;
20641
21531
  try {
20642
- const collectionsStmt = db.prepare("SELECT COUNT(*) as count FROM collections WHERE is_active = 1");
21532
+ const collectionsStmt = db.prepare("SELECT COUNT(*) as count FROM collections WHERE is_active = 1 AND (source_type IS NULL OR source_type = 'user')");
20643
21533
  const collectionsResult = await collectionsStmt.first();
20644
21534
  collectionsCount = collectionsResult?.count || 0;
20645
21535
  } catch (error) {
@@ -20647,7 +21537,7 @@ router.get("/stats", async (c) => {
20647
21537
  }
20648
21538
  let contentCount = 0;
20649
21539
  try {
20650
- const contentStmt = db.prepare("SELECT COUNT(*) as count FROM content");
21540
+ const contentStmt = db.prepare("SELECT COUNT(*) as count FROM content c JOIN collections col ON c.collection_id = col.id WHERE (col.source_type IS NULL OR col.source_type = 'user')");
20651
21541
  const contentResult = await contentStmt.first();
20652
21542
  contentCount = contentResult?.count || 0;
20653
21543
  } catch (error) {
@@ -22427,7 +23317,10 @@ function renderCollectionFormPage(data) {
22427
23317
 
22428
23318
  // src/routes/admin-collections.ts
22429
23319
  var adminCollectionsRoutes = new hono.Hono();
22430
- adminCollectionsRoutes.use("*", chunkDQZVU3WB_cjs.requireAuth());
23320
+ adminCollectionsRoutes.use("*", chunk5GO3AMON_cjs.requireAuth());
23321
+ adminCollectionsRoutes.post("*", chunk5GO3AMON_cjs.requireRole(["admin"]));
23322
+ adminCollectionsRoutes.put("*", chunk5GO3AMON_cjs.requireRole(["admin"]));
23323
+ adminCollectionsRoutes.delete("*", chunk5GO3AMON_cjs.requireRole(["admin"]));
22431
23324
  adminCollectionsRoutes.get("/", async (c) => {
22432
23325
  try {
22433
23326
  const user = c.get("user");
@@ -22441,6 +23334,7 @@ adminCollectionsRoutes.get("/", async (c) => {
22441
23334
  SELECT id, name, display_name, description, created_at, managed, schema
22442
23335
  FROM collections
22443
23336
  WHERE is_active = 1
23337
+ AND (source_type IS NULL OR source_type = 'user')
22444
23338
  AND (name LIKE ? OR display_name LIKE ? OR description LIKE ?)
22445
23339
  ORDER BY created_at DESC
22446
23340
  `);
@@ -22448,7 +23342,7 @@ adminCollectionsRoutes.get("/", async (c) => {
22448
23342
  const queryResults = await stmt.bind(searchParam, searchParam, searchParam).all();
22449
23343
  results = queryResults.results;
22450
23344
  } else {
22451
- stmt = db.prepare("SELECT id, name, display_name, description, created_at, managed, schema FROM collections WHERE is_active = 1 ORDER BY created_at DESC");
23345
+ stmt = db.prepare("SELECT id, name, display_name, description, created_at, managed, schema FROM collections WHERE is_active = 1 AND (source_type IS NULL OR source_type = 'user') ORDER BY created_at DESC");
22452
23346
  const queryResults = await stmt.all();
22453
23347
  results = queryResults.results;
22454
23348
  }
@@ -24622,7 +25516,7 @@ function renderDatabaseToolsSettings(settings) {
24622
25516
 
24623
25517
  // src/routes/admin-settings.ts
24624
25518
  var adminSettingsRoutes = new hono.Hono();
24625
- adminSettingsRoutes.use("*", chunkDQZVU3WB_cjs.requireAuth());
25519
+ adminSettingsRoutes.use("*", chunk5GO3AMON_cjs.requireAuth());
24626
25520
  function getMockSettings(user) {
24627
25521
  return {
24628
25522
  general: {
@@ -24687,7 +25581,7 @@ adminSettingsRoutes.get("/", (c) => {
24687
25581
  adminSettingsRoutes.get("/general", async (c) => {
24688
25582
  const user = c.get("user");
24689
25583
  const db = c.env.DB;
24690
- const settingsService = new chunk64APW3DW_cjs.SettingsService(db);
25584
+ const settingsService = new chunkLFAQUR7P_cjs.SettingsService(db);
24691
25585
  const generalSettings = await settingsService.getGeneralSettings(user?.email);
24692
25586
  const mockSettings = getMockSettings(user);
24693
25587
  mockSettings.general = generalSettings;
@@ -24790,7 +25684,7 @@ adminSettingsRoutes.get("/database-tools", (c) => {
24790
25684
  adminSettingsRoutes.get("/api/migrations/status", async (c) => {
24791
25685
  try {
24792
25686
  const db = c.env.DB;
24793
- const migrationService = new chunkLDFMYRG6_cjs.MigrationService(db);
25687
+ const migrationService = new chunkEAJJHE5F_cjs.MigrationService(db);
24794
25688
  const status = await migrationService.getMigrationStatus();
24795
25689
  return c.json({
24796
25690
  success: true,
@@ -24814,7 +25708,7 @@ adminSettingsRoutes.post("/api/migrations/run", async (c) => {
24814
25708
  }, 403);
24815
25709
  }
24816
25710
  const db = c.env.DB;
24817
- const migrationService = new chunkLDFMYRG6_cjs.MigrationService(db);
25711
+ const migrationService = new chunkEAJJHE5F_cjs.MigrationService(db);
24818
25712
  const result = await migrationService.runPendingMigrations();
24819
25713
  return c.json({
24820
25714
  success: result.success,
@@ -24832,7 +25726,7 @@ adminSettingsRoutes.post("/api/migrations/run", async (c) => {
24832
25726
  adminSettingsRoutes.get("/api/migrations/validate", async (c) => {
24833
25727
  try {
24834
25728
  const db = c.env.DB;
24835
- const migrationService = new chunkLDFMYRG6_cjs.MigrationService(db);
25729
+ const migrationService = new chunkEAJJHE5F_cjs.MigrationService(db);
24836
25730
  const validation = await migrationService.validateSchema();
24837
25731
  return c.json({
24838
25732
  success: true,
@@ -25001,7 +25895,7 @@ adminSettingsRoutes.post("/general", async (c) => {
25001
25895
  }
25002
25896
  const formData = await c.req.formData();
25003
25897
  const db = c.env.DB;
25004
- const settingsService = new chunk64APW3DW_cjs.SettingsService(db);
25898
+ const settingsService = new chunkLFAQUR7P_cjs.SettingsService(db);
25005
25899
  const settings = {
25006
25900
  siteName: formData.get("siteName"),
25007
25901
  siteDescription: formData.get("siteDescription"),
@@ -26722,7 +27616,7 @@ function renderFormCreatePage(data) {
26722
27616
 
26723
27617
  // src/routes/admin-forms.ts
26724
27618
  var adminFormsRoutes = new hono.Hono();
26725
- adminFormsRoutes.use("*", chunkDQZVU3WB_cjs.requireAuth());
27619
+ adminFormsRoutes.use("*", chunk5GO3AMON_cjs.requireAuth());
26726
27620
  adminFormsRoutes.get("/", async (c) => {
26727
27621
  try {
26728
27622
  const user = c.get("user");
@@ -27520,14 +28414,36 @@ publicFormsRoutes.post("/:identifier/submit", async (c) => {
27520
28414
  now
27521
28415
  ).run();
27522
28416
  await db.prepare(`
27523
- UPDATE forms
28417
+ UPDATE forms
27524
28418
  SET submission_count = submission_count + 1,
27525
28419
  updated_at = ?
27526
28420
  WHERE id = ?
27527
28421
  `).bind(now, form.id).run();
28422
+ let contentId = null;
28423
+ try {
28424
+ contentId = await chunkTWCQVJ6M_cjs.createContentFromSubmission(
28425
+ db,
28426
+ sanitizedData,
28427
+ { id: form.id, name: form.name, display_name: form.display_name },
28428
+ submissionId,
28429
+ {
28430
+ ipAddress: c.req.header("cf-connecting-ip") || null,
28431
+ userAgent: c.req.header("user-agent") || null,
28432
+ userEmail: sanitizedData?.email || null,
28433
+ userId: null
28434
+ // anonymous submission
28435
+ }
28436
+ );
28437
+ if (!contentId) {
28438
+ console.warn("[FormSubmit] Content creation returned null for submission:", submissionId);
28439
+ }
28440
+ } catch (contentError) {
28441
+ console.error("[FormSubmit] Error creating content from submission:", contentError);
28442
+ }
27528
28443
  return c.json({
27529
28444
  success: true,
27530
28445
  submissionId,
28446
+ contentId,
27531
28447
  message: "Form submitted successfully"
27532
28448
  });
27533
28449
  } catch (error) {
@@ -27679,7 +28595,7 @@ function renderAPIReferencePage(data) {
27679
28595
  >
27680
28596
  <option value="">All Categories</option>
27681
28597
  ${categories.map((category) => {
27682
- const info = chunk64APW3DW_cjs.CATEGORY_INFO[category];
28598
+ const info = chunkLFAQUR7P_cjs.CATEGORY_INFO[category];
27683
28599
  const title = info ? info.title : category;
27684
28600
  return `<option value="${category}">${title}</option>`;
27685
28601
  }).join("\n ")}
@@ -27696,7 +28612,7 @@ function renderAPIReferencePage(data) {
27696
28612
  <!-- API Categories -->
27697
28613
  <div class="space-y-6">
27698
28614
  ${Object.entries(endpointsByCategory).map(([category, endpoints]) => {
27699
- const info = chunk64APW3DW_cjs.CATEGORY_INFO[category] || { title: category, description: "", icon: "&#x1f4cb;" };
28615
+ const info = chunkLFAQUR7P_cjs.CATEGORY_INFO[category] || { title: category, description: "", icon: "&#x1f4cb;" };
27700
28616
  return `
27701
28617
  <div class="api-category" data-category="${category}">
27702
28618
  <div class="rounded-lg bg-white dark:bg-zinc-900 shadow-sm ring-1 ring-zinc-950/5 dark:ring-white/10 overflow-hidden">
@@ -27873,14 +28789,14 @@ function renderAPIReferencePage(data) {
27873
28789
  }
27874
28790
 
27875
28791
  // src/routes/admin-api-reference.ts
27876
- var VERSION2 = chunkSHU7Q66Q_cjs.getCoreVersion();
28792
+ var VERSION2 = chunkE2GKK5HX_cjs.getCoreVersion();
27877
28793
  var router2 = new hono.Hono();
27878
- router2.use("*", chunkDQZVU3WB_cjs.requireAuth());
28794
+ router2.use("*", chunk5GO3AMON_cjs.requireAuth());
27879
28795
  router2.get("/", async (c) => {
27880
28796
  const user = c.get("user");
27881
28797
  try {
27882
- const app2 = chunk64APW3DW_cjs.getAppInstance();
27883
- const endpoints = chunk64APW3DW_cjs.buildRouteList(app2);
28798
+ const app2 = chunkLFAQUR7P_cjs.getAppInstance();
28799
+ const endpoints = chunkLFAQUR7P_cjs.buildRouteList(app2);
27884
28800
  const pageData = {
27885
28801
  endpoints,
27886
28802
  user: user ? {
@@ -27962,5 +28878,5 @@ exports.router = router;
27962
28878
  exports.router2 = router2;
27963
28879
  exports.test_cleanup_default = test_cleanup_default;
27964
28880
  exports.userRoutes = userRoutes;
27965
- //# sourceMappingURL=chunk-KSB6FXOP.cjs.map
27966
- //# sourceMappingURL=chunk-KSB6FXOP.cjs.map
28881
+ //# sourceMappingURL=chunk-HGKBMUYY.cjs.map
28882
+ //# sourceMappingURL=chunk-HGKBMUYY.cjs.map