@sonicjs-cms/core 2.9.0 → 2.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/{chunk-YFJJU26H.js → chunk-27AOVQTR.js} +10 -2
  2. package/dist/chunk-27AOVQTR.js.map +1 -0
  3. package/dist/{chunk-25YNV4RK.js → chunk-4TTMQQC7.js} +4 -4
  4. package/dist/{chunk-25YNV4RK.js.map → chunk-4TTMQQC7.js.map} +1 -1
  5. package/dist/{chunk-STTZVLY2.js → chunk-6O3RJV3C.js} +2 -2
  6. package/dist/{chunk-STTZVLY2.js.map → chunk-6O3RJV3C.js.map} +1 -1
  7. package/dist/{chunk-SHU7Q66Q.cjs → chunk-EKPLKUZT.cjs} +7 -3
  8. package/dist/chunk-EKPLKUZT.cjs.map +1 -0
  9. package/dist/{chunk-MPT5PA6U.cjs → chunk-IIBRG5S5.cjs} +10 -2
  10. package/dist/chunk-IIBRG5S5.cjs.map +1 -0
  11. package/dist/{chunk-DQZVU3WB.cjs → chunk-IT2TC4ZD.cjs} +7 -7
  12. package/dist/{chunk-DQZVU3WB.cjs.map → chunk-IT2TC4ZD.cjs.map} +1 -1
  13. package/dist/{chunk-3FHMXGLF.js → chunk-IZWNIUJI.js} +7 -3
  14. package/dist/chunk-IZWNIUJI.js.map +1 -0
  15. package/dist/{chunk-2JGQKF7B.js → chunk-JTNUM7JE.js} +1042 -152
  16. package/dist/chunk-JTNUM7JE.js.map +1 -0
  17. package/dist/{chunk-KSB6FXOP.cjs → chunk-RCA6R6VE.cjs} +1130 -240
  18. package/dist/chunk-RCA6R6VE.cjs.map +1 -0
  19. package/dist/{chunk-LDFMYRG6.cjs → chunk-ZMVWMJ3S.cjs} +2 -2
  20. package/dist/{chunk-LDFMYRG6.cjs.map → chunk-ZMVWMJ3S.cjs.map} +1 -1
  21. package/dist/{collection-config-DckWhkdL.d.cts → collection-config-B4PG-AaF.d.cts} +2 -0
  22. package/dist/{collection-config-DckWhkdL.d.ts → collection-config-B4PG-AaF.d.ts} +2 -0
  23. package/dist/index.cjs +97 -97
  24. package/dist/index.d.cts +3 -3
  25. package/dist/index.d.ts +3 -3
  26. package/dist/index.js +8 -8
  27. package/dist/middleware.cjs +29 -29
  28. package/dist/middleware.js +3 -3
  29. package/dist/migrations-N2C2VPJU.js +4 -0
  30. package/dist/{migrations-SZSR3C3G.js.map → migrations-N2C2VPJU.js.map} +1 -1
  31. package/dist/migrations-ONIAY6GK.cjs +13 -0
  32. package/dist/{migrations-QQWGDWGB.cjs.map → migrations-ONIAY6GK.cjs.map} +1 -1
  33. package/dist/{plugin-bootstrap-BAz7NY0H.d.cts → plugin-bootstrap-WmpvYM5w.d.ts} +1 -1
  34. package/dist/{plugin-bootstrap-Cz3-bj8X.d.ts → plugin-bootstrap-fpG98Otb.d.cts} +1 -1
  35. package/dist/routes.cjs +28 -28
  36. package/dist/routes.js +5 -5
  37. package/dist/services.cjs +16 -16
  38. package/dist/services.d.cts +2 -2
  39. package/dist/services.d.ts +2 -2
  40. package/dist/services.js +2 -2
  41. package/dist/types.d.cts +1 -1
  42. package/dist/types.d.ts +1 -1
  43. package/dist/utils.cjs +11 -11
  44. package/dist/utils.d.cts +1 -1
  45. package/dist/utils.d.ts +1 -1
  46. package/dist/utils.js +1 -1
  47. package/package.json +5 -1
  48. package/dist/chunk-2JGQKF7B.js.map +0 -1
  49. package/dist/chunk-3FHMXGLF.js.map +0 -1
  50. package/dist/chunk-KSB6FXOP.cjs.map +0 -1
  51. package/dist/chunk-MPT5PA6U.cjs.map +0 -1
  52. package/dist/chunk-SHU7Q66Q.cjs.map +0 -1
  53. package/dist/chunk-YFJJU26H.js.map +0 -1
  54. package/dist/migrations-QQWGDWGB.cjs +0 -13
  55. package/dist/migrations-SZSR3C3G.js +0 -4
@@ -1,12 +1,12 @@
1
1
  'use strict';
2
2
 
3
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');
4
+ var chunkIT2TC4ZD_cjs = require('./chunk-IT2TC4ZD.cjs');
5
+ var chunkIIBRG5S5_cjs = require('./chunk-IIBRG5S5.cjs');
6
+ var chunkZMVWMJ3S_cjs = require('./chunk-ZMVWMJ3S.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 chunkEKPLKUZT_cjs = require('./chunk-EKPLKUZT.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("/", chunkIT2TC4ZD_cjs.requireAuth(), chunkIT2TC4ZD_cjs.requireRole(["admin", "editor", "author"]), async (c) => {
125
125
  try {
126
126
  const db = c.env.DB;
127
127
  const user = c.get("user");
@@ -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", chunkIT2TC4ZD_cjs.requireAuth(), chunkIT2TC4ZD_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;
@@ -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", chunkIT2TC4ZD_cjs.requireAuth(), chunkIT2TC4ZD_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;
@@ -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 chunkIT2TC4ZD_cjs.isPluginActive(c.env.DB, "core-cache");
291
291
  c.set("cacheEnabled", cacheEnabled);
292
292
  await next();
293
293
  });
@@ -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", chunkIT2TC4ZD_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 = chunkEKPLKUZT_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 chunkEKPLKUZT_cjs.QueryFilterBuilder();
811
811
  const queryResult = builder3.build("content", normalizedFilter);
812
812
  if (queryResult.errors.length > 0) {
813
813
  return c.json({
@@ -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", chunkIT2TC4ZD_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 = chunkEKPLKUZT_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 chunkEKPLKUZT_cjs.QueryFilterBuilder();
911
911
  const queryResult = builder3.build("content", normalizedFilter);
912
912
  if (queryResult.errors.length > 0) {
913
913
  return c.json({
@@ -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("*", chunkIT2TC4ZD_cjs.requireAuth());
1032
1032
  apiMediaRoutes.post("/upload", async (c) => {
1033
1033
  try {
1034
1034
  const user = c.get("user");
@@ -1772,8 +1772,8 @@ 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("*", chunkIT2TC4ZD_cjs.requireAuth());
1776
+ adminApiRoutes.use("*", chunkIT2TC4ZD_cjs.requireRole(["admin", "editor"]));
1777
1777
  adminApiRoutes.get("/stats", async (c) => {
1778
1778
  try {
1779
1779
  const db = c.env.DB;
@@ -2283,7 +2283,7 @@ adminApiRoutes.delete("/collections/:id", async (c) => {
2283
2283
  });
2284
2284
  adminApiRoutes.get("/migrations/status", async (c) => {
2285
2285
  try {
2286
- const { MigrationService: MigrationService2 } = await import('./migrations-QQWGDWGB.cjs');
2286
+ const { MigrationService: MigrationService2 } = await import('./migrations-ONIAY6GK.cjs');
2287
2287
  const db = c.env.DB;
2288
2288
  const migrationService = new MigrationService2(db);
2289
2289
  const status = await migrationService.getMigrationStatus();
@@ -2308,7 +2308,7 @@ adminApiRoutes.post("/migrations/run", async (c) => {
2308
2308
  error: "Unauthorized. Admin access required."
2309
2309
  }, 403);
2310
2310
  }
2311
- const { MigrationService: MigrationService2 } = await import('./migrations-QQWGDWGB.cjs');
2311
+ const { MigrationService: MigrationService2 } = await import('./migrations-ONIAY6GK.cjs');
2312
2312
  const db = c.env.DB;
2313
2313
  const migrationService = new MigrationService2(db);
2314
2314
  const result = await migrationService.runPendingMigrations();
@@ -2327,7 +2327,7 @@ adminApiRoutes.post("/migrations/run", async (c) => {
2327
2327
  });
2328
2328
  adminApiRoutes.get("/migrations/validate", async (c) => {
2329
2329
  try {
2330
- const { MigrationService: MigrationService2 } = await import('./migrations-QQWGDWGB.cjs');
2330
+ const { MigrationService: MigrationService2 } = await import('./migrations-ONIAY6GK.cjs');
2331
2331
  const db = c.env.DB;
2332
2332
  const migrationService = new MigrationService2(db);
2333
2333
  const validation = await migrationService.validateSchema();
@@ -2738,7 +2738,7 @@ var JWT_SECRET_FALLBACK = "your-super-secret-jwt-key-change-in-production";
2738
2738
  async function setCsrfCookie(c) {
2739
2739
  const secret = c.env?.JWT_SECRET || JWT_SECRET_FALLBACK;
2740
2740
  const isDev = c.env?.ENVIRONMENT === "development" || !c.env?.ENVIRONMENT;
2741
- const csrfToken = await chunkDQZVU3WB_cjs.generateCsrfToken(secret);
2741
+ const csrfToken = await chunkIT2TC4ZD_cjs.generateCsrfToken(secret);
2742
2742
  cookie.setCookie(c, "csrf_token", csrfToken, {
2743
2743
  httpOnly: false,
2744
2744
  secure: !isDev,
@@ -2795,7 +2795,7 @@ var loginSchema = zod.z.object({
2795
2795
  });
2796
2796
  authRoutes.post(
2797
2797
  "/register",
2798
- chunkDQZVU3WB_cjs.rateLimit({ max: 3, windowMs: 60 * 1e3, keyPrefix: "register" }),
2798
+ chunkIT2TC4ZD_cjs.rateLimit({ max: 3, windowMs: 60 * 1e3, keyPrefix: "register" }),
2799
2799
  async (c) => {
2800
2800
  try {
2801
2801
  const db = c.env.DB;
@@ -2832,7 +2832,7 @@ authRoutes.post(
2832
2832
  if (existingUser) {
2833
2833
  return c.json({ error: "User with this email or username already exists" }, 400);
2834
2834
  }
2835
- const passwordHash = await chunkDQZVU3WB_cjs.AuthManager.hashPassword(password);
2835
+ const passwordHash = await chunkIT2TC4ZD_cjs.AuthManager.hashPassword(password);
2836
2836
  const userId = crypto.randomUUID();
2837
2837
  const now = /* @__PURE__ */ new Date();
2838
2838
  await db.prepare(`
@@ -2852,7 +2852,7 @@ authRoutes.post(
2852
2852
  now.getTime(),
2853
2853
  now.getTime()
2854
2854
  ).run();
2855
- const token = await chunkDQZVU3WB_cjs.AuthManager.generateToken(userId, normalizedEmail, "viewer", c.env.JWT_SECRET);
2855
+ const token = await chunkIT2TC4ZD_cjs.AuthManager.generateToken(userId, normalizedEmail, "viewer", c.env.JWT_SECRET);
2856
2856
  cookie.setCookie(c, "auth_token", token, {
2857
2857
  httpOnly: true,
2858
2858
  secure: true,
@@ -2886,7 +2886,7 @@ authRoutes.post(
2886
2886
  );
2887
2887
  authRoutes.post(
2888
2888
  "/login",
2889
- chunkDQZVU3WB_cjs.rateLimit({ max: 5, windowMs: 60 * 1e3, keyPrefix: "login" }),
2889
+ chunkIT2TC4ZD_cjs.rateLimit({ max: 5, windowMs: 60 * 1e3, keyPrefix: "login" }),
2890
2890
  async (c) => {
2891
2891
  try {
2892
2892
  const body = await c.req.json();
@@ -2909,19 +2909,19 @@ authRoutes.post(
2909
2909
  if (!user) {
2910
2910
  return c.json({ error: "Invalid email or password" }, 401);
2911
2911
  }
2912
- const isValidPassword = await chunkDQZVU3WB_cjs.AuthManager.verifyPassword(password, user.password_hash);
2912
+ const isValidPassword = await chunkIT2TC4ZD_cjs.AuthManager.verifyPassword(password, user.password_hash);
2913
2913
  if (!isValidPassword) {
2914
2914
  return c.json({ error: "Invalid email or password" }, 401);
2915
2915
  }
2916
- if (chunkDQZVU3WB_cjs.AuthManager.isLegacyHash(user.password_hash)) {
2916
+ if (chunkIT2TC4ZD_cjs.AuthManager.isLegacyHash(user.password_hash)) {
2917
2917
  try {
2918
- const newHash = await chunkDQZVU3WB_cjs.AuthManager.hashPassword(password);
2918
+ const newHash = await chunkIT2TC4ZD_cjs.AuthManager.hashPassword(password);
2919
2919
  await db.prepare("UPDATE users SET password_hash = ?, updated_at = ? WHERE id = ?").bind(newHash, Date.now(), user.id).run();
2920
2920
  } catch (rehashError) {
2921
2921
  console.error("Password rehash failed (non-fatal):", rehashError);
2922
2922
  }
2923
2923
  }
2924
- const token = await chunkDQZVU3WB_cjs.AuthManager.generateToken(user.id, user.email, user.role, c.env.JWT_SECRET);
2924
+ const token = await chunkIT2TC4ZD_cjs.AuthManager.generateToken(user.id, user.email, user.role, c.env.JWT_SECRET);
2925
2925
  cookie.setCookie(c, "auth_token", token, {
2926
2926
  httpOnly: true,
2927
2927
  secure: true,
@@ -2974,7 +2974,7 @@ authRoutes.get("/logout", (c) => {
2974
2974
  clearCsrfCookie(c);
2975
2975
  return c.redirect("/auth/login?message=You have been logged out successfully");
2976
2976
  });
2977
- authRoutes.get("/me", chunkDQZVU3WB_cjs.requireAuth(), async (c) => {
2977
+ authRoutes.get("/me", chunkIT2TC4ZD_cjs.requireAuth(), async (c) => {
2978
2978
  try {
2979
2979
  const user = c.get("user");
2980
2980
  if (!user) {
@@ -2991,13 +2991,13 @@ authRoutes.get("/me", chunkDQZVU3WB_cjs.requireAuth(), async (c) => {
2991
2991
  return c.json({ error: "Failed to get user" }, 500);
2992
2992
  }
2993
2993
  });
2994
- authRoutes.post("/refresh", chunkDQZVU3WB_cjs.requireAuth(), async (c) => {
2994
+ authRoutes.post("/refresh", chunkIT2TC4ZD_cjs.requireAuth(), async (c) => {
2995
2995
  try {
2996
2996
  const user = c.get("user");
2997
2997
  if (!user) {
2998
2998
  return c.json({ error: "Not authenticated" }, 401);
2999
2999
  }
3000
- const token = await chunkDQZVU3WB_cjs.AuthManager.generateToken(user.userId, user.email, user.role, c.env.JWT_SECRET);
3000
+ const token = await chunkIT2TC4ZD_cjs.AuthManager.generateToken(user.userId, user.email, user.role, c.env.JWT_SECRET);
3001
3001
  cookie.setCookie(c, "auth_token", token, {
3002
3002
  httpOnly: true,
3003
3003
  secure: true,
@@ -3014,7 +3014,7 @@ authRoutes.post("/refresh", chunkDQZVU3WB_cjs.requireAuth(), async (c) => {
3014
3014
  });
3015
3015
  authRoutes.post(
3016
3016
  "/register/form",
3017
- chunkDQZVU3WB_cjs.rateLimit({ max: 3, windowMs: 60 * 1e3, keyPrefix: "register" }),
3017
+ chunkIT2TC4ZD_cjs.rateLimit({ max: 3, windowMs: 60 * 1e3, keyPrefix: "register" }),
3018
3018
  async (c) => {
3019
3019
  try {
3020
3020
  const db = c.env.DB;
@@ -3061,7 +3061,7 @@ authRoutes.post(
3061
3061
  </div>
3062
3062
  `);
3063
3063
  }
3064
- const passwordHash = await chunkDQZVU3WB_cjs.AuthManager.hashPassword(password);
3064
+ const passwordHash = await chunkIT2TC4ZD_cjs.AuthManager.hashPassword(password);
3065
3065
  const role = isFirstUser ? "admin" : "viewer";
3066
3066
  const userId = crypto.randomUUID();
3067
3067
  const now = /* @__PURE__ */ new Date();
@@ -3081,7 +3081,7 @@ authRoutes.post(
3081
3081
  now.getTime(),
3082
3082
  now.getTime()
3083
3083
  ).run();
3084
- const token = await chunkDQZVU3WB_cjs.AuthManager.generateToken(userId, normalizedEmail, role, c.env.JWT_SECRET);
3084
+ const token = await chunkIT2TC4ZD_cjs.AuthManager.generateToken(userId, normalizedEmail, role, c.env.JWT_SECRET);
3085
3085
  cookie.setCookie(c, "auth_token", token, {
3086
3086
  httpOnly: true,
3087
3087
  secure: false,
@@ -3114,7 +3114,7 @@ authRoutes.post(
3114
3114
  );
3115
3115
  authRoutes.post(
3116
3116
  "/login/form",
3117
- chunkDQZVU3WB_cjs.rateLimit({ max: 5, windowMs: 60 * 1e3, keyPrefix: "login" }),
3117
+ chunkIT2TC4ZD_cjs.rateLimit({ max: 5, windowMs: 60 * 1e3, keyPrefix: "login" }),
3118
3118
  async (c) => {
3119
3119
  try {
3120
3120
  const formData = await c.req.formData();
@@ -3138,7 +3138,7 @@ authRoutes.post(
3138
3138
  </div>
3139
3139
  `);
3140
3140
  }
3141
- const isValidPassword = await chunkDQZVU3WB_cjs.AuthManager.verifyPassword(password, user.password_hash);
3141
+ const isValidPassword = await chunkIT2TC4ZD_cjs.AuthManager.verifyPassword(password, user.password_hash);
3142
3142
  if (!isValidPassword) {
3143
3143
  return c.html(html.html`
3144
3144
  <div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
@@ -3146,15 +3146,15 @@ authRoutes.post(
3146
3146
  </div>
3147
3147
  `);
3148
3148
  }
3149
- if (chunkDQZVU3WB_cjs.AuthManager.isLegacyHash(user.password_hash)) {
3149
+ if (chunkIT2TC4ZD_cjs.AuthManager.isLegacyHash(user.password_hash)) {
3150
3150
  try {
3151
- const newHash = await chunkDQZVU3WB_cjs.AuthManager.hashPassword(password);
3151
+ const newHash = await chunkIT2TC4ZD_cjs.AuthManager.hashPassword(password);
3152
3152
  await db.prepare("UPDATE users SET password_hash = ?, updated_at = ? WHERE id = ?").bind(newHash, Date.now(), user.id).run();
3153
3153
  } catch (rehashError) {
3154
3154
  console.error("Password rehash failed (non-fatal):", rehashError);
3155
3155
  }
3156
3156
  }
3157
- const token = await chunkDQZVU3WB_cjs.AuthManager.generateToken(user.id, user.email, user.role, c.env.JWT_SECRET);
3157
+ const token = await chunkIT2TC4ZD_cjs.AuthManager.generateToken(user.id, user.email, user.role, c.env.JWT_SECRET);
3158
3158
  cookie.setCookie(c, "auth_token", token, {
3159
3159
  httpOnly: true,
3160
3160
  secure: false,
@@ -3196,7 +3196,7 @@ authRoutes.post(
3196
3196
  );
3197
3197
  authRoutes.post(
3198
3198
  "/seed-admin",
3199
- chunkDQZVU3WB_cjs.rateLimit({ max: 2, windowMs: 60 * 1e3, keyPrefix: "seed-admin" }),
3199
+ chunkIT2TC4ZD_cjs.rateLimit({ max: 2, windowMs: 60 * 1e3, keyPrefix: "seed-admin" }),
3200
3200
  async (c) => {
3201
3201
  try {
3202
3202
  const db = c.env.DB;
@@ -3218,7 +3218,7 @@ authRoutes.post(
3218
3218
  `).run();
3219
3219
  const existingAdmin = await db.prepare("SELECT id FROM users WHERE email = ? OR username = ?").bind("admin@sonicjs.com", "admin").first();
3220
3220
  if (existingAdmin) {
3221
- const passwordHash2 = await chunkDQZVU3WB_cjs.AuthManager.hashPassword("sonicjs!");
3221
+ const passwordHash2 = await chunkIT2TC4ZD_cjs.AuthManager.hashPassword("sonicjs!");
3222
3222
  await db.prepare("UPDATE users SET password_hash = ?, updated_at = ? WHERE id = ?").bind(passwordHash2, Date.now(), existingAdmin.id).run();
3223
3223
  return c.json({
3224
3224
  message: "Admin user already exists (password updated)",
@@ -3230,7 +3230,7 @@ authRoutes.post(
3230
3230
  }
3231
3231
  });
3232
3232
  }
3233
- const passwordHash = await chunkDQZVU3WB_cjs.AuthManager.hashPassword("sonicjs!");
3233
+ const passwordHash = await chunkIT2TC4ZD_cjs.AuthManager.hashPassword("sonicjs!");
3234
3234
  const userId = "admin-user-id";
3235
3235
  const now = Date.now();
3236
3236
  const adminEmail = "admin@sonicjs.com".toLowerCase();
@@ -3451,7 +3451,7 @@ authRoutes.post("/accept-invitation", async (c) => {
3451
3451
  if (existingUsername) {
3452
3452
  return c.json({ error: "Username is already taken" }, 400);
3453
3453
  }
3454
- const passwordHash = await chunkDQZVU3WB_cjs.AuthManager.hashPassword(password);
3454
+ const passwordHash = await chunkIT2TC4ZD_cjs.AuthManager.hashPassword(password);
3455
3455
  const updateStmt = db.prepare(`
3456
3456
  UPDATE users SET
3457
3457
  username = ?,
@@ -3470,7 +3470,7 @@ authRoutes.post("/accept-invitation", async (c) => {
3470
3470
  Date.now(),
3471
3471
  invitedUser.id
3472
3472
  ).run();
3473
- const authToken = await chunkDQZVU3WB_cjs.AuthManager.generateToken(invitedUser.id, invitedUser.email, invitedUser.role, c.env.JWT_SECRET);
3473
+ const authToken = await chunkIT2TC4ZD_cjs.AuthManager.generateToken(invitedUser.id, invitedUser.email, invitedUser.role, c.env.JWT_SECRET);
3474
3474
  cookie.setCookie(c, "auth_token", authToken, {
3475
3475
  httpOnly: true,
3476
3476
  secure: true,
@@ -3487,7 +3487,7 @@ authRoutes.post("/accept-invitation", async (c) => {
3487
3487
  });
3488
3488
  authRoutes.post(
3489
3489
  "/request-password-reset",
3490
- chunkDQZVU3WB_cjs.rateLimit({ max: 3, windowMs: 15 * 60 * 1e3, keyPrefix: "password-reset" }),
3490
+ chunkIT2TC4ZD_cjs.rateLimit({ max: 3, windowMs: 15 * 60 * 1e3, keyPrefix: "password-reset" }),
3491
3491
  async (c) => {
3492
3492
  try {
3493
3493
  const formData = await c.req.formData();
@@ -3705,7 +3705,7 @@ authRoutes.post("/reset-password", async (c) => {
3705
3705
  if (Date.now() > user.password_reset_expires) {
3706
3706
  return c.json({ error: "Reset token has expired" }, 400);
3707
3707
  }
3708
- const newPasswordHash = await chunkDQZVU3WB_cjs.AuthManager.hashPassword(password);
3708
+ const newPasswordHash = await chunkIT2TC4ZD_cjs.AuthManager.hashPassword(password);
3709
3709
  try {
3710
3710
  const historyStmt = db.prepare(`
3711
3711
  INSERT INTO password_history (id, user_id, password_hash, created_at)
@@ -4777,6 +4777,39 @@ function getReadFieldValueScript() {
4777
4777
  window.__sonicReadFieldValueInit = true;
4778
4778
 
4779
4779
  window.sonicReadFieldValue = function(fieldWrapper) {
4780
+ const getDirectChild = (parent, selector) => {
4781
+ if (!(parent instanceof Element)) return null;
4782
+ return Array.from(parent.children).find(
4783
+ (child) => child instanceof Element && child.matches(selector),
4784
+ ) || null;
4785
+ };
4786
+ const getDirectStructuredSubfields = (host) =>
4787
+ Array.from(host.children).filter(
4788
+ (child) => child instanceof Element && child.classList.contains('structured-subfield'),
4789
+ );
4790
+ const getStructuredObjectFieldsHost = (container) => {
4791
+ const directFieldsHost = getDirectChild(container, '[data-structured-object-fields]');
4792
+ if (directFieldsHost) return directFieldsHost;
4793
+ const groupContent = getDirectChild(container, '.field-group-content');
4794
+ const nestedFieldsHost = groupContent
4795
+ ? getDirectChild(groupContent, '[data-structured-object-fields]')
4796
+ : null;
4797
+ if (nestedFieldsHost) return nestedFieldsHost;
4798
+ return getDirectChild(container, '[data-array-item-fields]') || container;
4799
+ };
4800
+ const getDirectStructuredObject = (fieldWrapper) => {
4801
+ const directObject = getDirectChild(fieldWrapper, '[data-structured-object]');
4802
+ if (directObject) return directObject;
4803
+ const formGroup = getDirectChild(fieldWrapper, '.form-group');
4804
+ return formGroup ? getDirectChild(formGroup, '[data-structured-object]') : null;
4805
+ };
4806
+ const getDirectStructuredArray = (fieldWrapper) => {
4807
+ const directArray = getDirectChild(fieldWrapper, '[data-structured-array]');
4808
+ if (directArray) return directArray;
4809
+ const formGroup = getDirectChild(fieldWrapper, '.form-group');
4810
+ return formGroup ? getDirectChild(formGroup, '[data-structured-array]') : null;
4811
+ };
4812
+
4780
4813
  const fieldType = fieldWrapper.dataset.fieldType;
4781
4814
  const select = fieldWrapper.querySelector('select');
4782
4815
  const textarea = fieldWrapper.querySelector('textarea');
@@ -4786,7 +4819,47 @@ function getReadFieldValueScript() {
4786
4819
  const nonHiddenInput = inputs.find((input) => input.type !== 'hidden' && input.type !== 'checkbox');
4787
4820
  const hiddenInput = inputs.find((input) => input.type === 'hidden');
4788
4821
 
4822
+ const readStructuredFieldsHost = (host) => {
4823
+ const fields = getDirectStructuredSubfields(host);
4824
+ if (fields.length === 1 && fields[0].dataset.structuredField === '__value') {
4825
+ return window.sonicReadFieldValue(fields[0]);
4826
+ }
4827
+ return fields.reduce((acc, subfield) => {
4828
+ const fieldName = subfield.dataset.structuredField;
4829
+ if (!fieldName || fieldName === '__value') return acc;
4830
+ acc[fieldName] = window.sonicReadFieldValue(subfield);
4831
+ return acc;
4832
+ }, {});
4833
+ };
4834
+
4835
+ const readStructuredObject = () => {
4836
+ const objectContainer = getDirectStructuredObject(fieldWrapper);
4837
+ if (!objectContainer) return null;
4838
+ const host = getStructuredObjectFieldsHost(objectContainer);
4839
+ return readStructuredFieldsHost(host);
4840
+ };
4841
+
4842
+ const readStructuredArray = () => {
4843
+ const arrayContainer = getDirectStructuredArray(fieldWrapper);
4844
+ if (!arrayContainer) return null;
4845
+ const list = arrayContainer.querySelector('[data-structured-array-list]');
4846
+ if (!list) return [];
4847
+ const items = Array.from(list.querySelectorAll(':scope > .structured-array-item'));
4848
+ return items.map((item) => {
4849
+ const host =
4850
+ item.querySelector(':scope > [data-array-item-fields]') ||
4851
+ item.querySelector('[data-array-item-fields]') ||
4852
+ item;
4853
+ return readStructuredFieldsHost(host);
4854
+ });
4855
+ };
4856
+
4789
4857
  if (fieldType === 'object' || fieldType === 'array') {
4858
+ const liveValue = fieldType === 'array' ? readStructuredArray() : readStructuredObject();
4859
+ if (liveValue !== null) {
4860
+ return liveValue;
4861
+ }
4862
+
4790
4863
  if (!hiddenInput) {
4791
4864
  return fieldType === 'array' ? [] : {};
4792
4865
  }
@@ -4835,6 +4908,15 @@ function getReadFieldValueScript() {
4835
4908
  </script>
4836
4909
  `;
4837
4910
  }
4911
+ var STRUCTURED_INDEX_TOKEN = "__INDEX__";
4912
+ var BLOCK_INDEX_TOKEN = "__BLOCK_INDEX__";
4913
+ function sanitizeStructuredGroupId(fieldName) {
4914
+ return `object-${fieldName}`.split(BLOCK_INDEX_TOKEN).map(
4915
+ (blockSegment) => blockSegment.split(STRUCTURED_INDEX_TOKEN).map(
4916
+ (segment) => segment.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "")
4917
+ ).join(STRUCTURED_INDEX_TOKEN)
4918
+ ).join(BLOCK_INDEX_TOKEN);
4919
+ }
4838
4920
  function isMarkdownEditorFieldType(fieldType) {
4839
4921
  return fieldType === "markdown" || fieldType === "mdxeditor" || fieldType === "easymde";
4840
4922
  }
@@ -5415,12 +5497,14 @@ function renderDynamicField(field, options = {}) {
5415
5497
 
5416
5498
  ${isMultiple ? `
5417
5499
  <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) => `
5500
+ ${mediaValues.map(
5501
+ (url, idx) => `
5419
5502
  <div class="relative media-preview-item" data-url="${url}">
5420
5503
  ${renderMediaPreview(url, `Media ${idx + 1}`, "w-full h-24 object-cover rounded-lg border border-white/20")}
5421
5504
  <button
5422
5505
  type="button"
5423
5506
  onclick="removeMediaFromMultiple('${fieldId}', '${url}')"
5507
+ data-media-remove="true"
5424
5508
  class="absolute top-1 right-1 bg-red-600 text-white rounded-full p-1 hover:bg-red-700"
5425
5509
  ${disabled ? "disabled" : ""}
5426
5510
  >
@@ -5429,7 +5513,8 @@ function renderDynamicField(field, options = {}) {
5429
5513
  </svg>
5430
5514
  </button>
5431
5515
  </div>
5432
- `).join("")}
5516
+ `
5517
+ ).join("")}
5433
5518
  </div>
5434
5519
  ` : `
5435
5520
  <div class="media-preview ${singleValue ? "" : "hidden"}" id="${fieldId}-preview">
@@ -5453,6 +5538,7 @@ function renderDynamicField(field, options = {}) {
5453
5538
  <button
5454
5539
  type="button"
5455
5540
  onclick="clearMediaField('${fieldId}')"
5541
+ data-media-remove="true"
5456
5542
  class="inline-flex items-center px-4 py-2 bg-red-600 text-white rounded-xl hover:bg-red-700 transition-all"
5457
5543
  ${disabled ? "disabled" : ""}
5458
5544
  >
@@ -5486,7 +5572,7 @@ function renderDynamicField(field, options = {}) {
5486
5572
  }
5487
5573
  const showLabel = field.field_type !== "boolean";
5488
5574
  return `
5489
- <div class="form-group">
5575
+ <div class="form-group" data-has-errors="${errors.length > 0 ? "true" : "false"}">
5490
5576
  ${showLabel ? `
5491
5577
  <label for="${fieldId}" class="block text-sm/6 font-medium text-zinc-950 dark:text-white mb-2">
5492
5578
  ${escapeHtml3(field.field_label)}
@@ -5495,7 +5581,7 @@ function renderDynamicField(field, options = {}) {
5495
5581
  ` : ""}
5496
5582
  ${fieldHTML}
5497
5583
  ${errors.length > 0 ? `
5498
- <div class="mt-2 text-sm text-pink-600 dark:text-pink-400">
5584
+ <div class="mt-2 text-sm text-pink-600 dark:text-pink-400" data-validation-error-message>
5499
5585
  ${errors.map((error) => `<div>${escapeHtml3(error)}</div>`).join("")}
5500
5586
  </div>
5501
5587
  ` : ""}
@@ -5510,8 +5596,8 @@ function renderDynamicField(field, options = {}) {
5510
5596
  function renderFieldGroup(title, fields, collapsible = false) {
5511
5597
  const groupId = title.toLowerCase().replace(/\s+/g, "-");
5512
5598
  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}')"` : ""}>
5599
+ <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)}">
5600
+ <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
5601
  <h3 class="text-base/7 font-semibold text-zinc-950 dark:text-white flex items-center">
5516
5602
  ${escapeHtml3(title)}
5517
5603
  ${collapsible ? `
@@ -5555,6 +5641,12 @@ function renderBlocksField(field, options, baseClasses, errorClasses) {
5555
5641
  >
5556
5642
  <input type="hidden" id="${fieldId}" name="${fieldName}" value="${escapeHtml3(JSON.stringify(blockValues))}">
5557
5643
 
5644
+ <div class="flex items-center justify-between border-b border-zinc-950/5 dark:border-white/10 py-4">
5645
+ <h3 class="text-base/7 font-semibold text-zinc-950 dark:text-white">
5646
+ ${escapeHtml3(field.field_label || "Content Blocks")}
5647
+ </h3>
5648
+ </div>
5649
+
5558
5650
  <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
5559
5651
  <div class="flex-1">
5560
5652
  <select
@@ -5585,12 +5677,14 @@ function renderBlocksField(field, options, baseClasses, errorClasses) {
5585
5677
  `;
5586
5678
  }
5587
5679
  function renderStructuredObjectField(field, options, baseClasses, errorClasses) {
5588
- const { value = {}, pluginStatuses = {} } = options;
5680
+ const { value = {}, pluginStatuses = {}, errors = [] } = options;
5589
5681
  const opts = field.field_options || {};
5590
5682
  const properties = opts.properties && typeof opts.properties === "object" ? opts.properties : {};
5591
5683
  const fieldId = `field-${field.field_name}`;
5592
5684
  const fieldName = field.field_name;
5593
5685
  const objectValue = normalizeStructuredObjectValue(value);
5686
+ const objectLayout = opts.objectLayout || "nested";
5687
+ const useNestedLayout = objectLayout !== "flat";
5594
5688
  const subfields = Object.entries(properties).map(
5595
5689
  ([propertyName, propertyConfig]) => renderStructuredSubfield(
5596
5690
  field,
@@ -5601,11 +5695,40 @@ function renderStructuredObjectField(field, options, baseClasses, errorClasses)
5601
5695
  field.field_name
5602
5696
  )
5603
5697
  ).join("");
5698
+ const groupTitle = field.field_label || field.field_name;
5699
+ if (!useNestedLayout) {
5700
+ return `
5701
+ <div class="space-y-4" data-structured-object data-field-name="${escapeHtml3(fieldName)}">
5702
+ <input type="hidden" id="${fieldId}" name="${fieldName}" value="${escapeHtml3(JSON.stringify(objectValue))}">
5703
+ <div class="flex items-center justify-between border-b border-zinc-950/5 dark:border-white/10 py-4 first-of-type:pt-0">
5704
+ <h3 class="text-base/7 font-semibold text-zinc-950 dark:text-white">
5705
+ ${escapeHtml3(groupTitle)}
5706
+ </h3>
5707
+ </div>
5708
+ <div class="space-y-4" data-structured-object-fields>
5709
+ ${subfields}
5710
+ </div>
5711
+ </div>
5712
+ ${getStructuredFieldScript()}
5713
+ `;
5714
+ }
5715
+ const groupId = sanitizeStructuredGroupId(field.field_name);
5716
+ const isCollapsed = errors.length > 0 ? false : opts.collapsed !== false;
5604
5717
  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}
5718
+ <div class="field-group rounded-lg shadow-sm mb-6" data-group-id="${escapeHtml3(groupId)}" data-structured-object data-field-name="${escapeHtml3(fieldName)}">
5719
+ <div class="field-group-header border-b border-zinc-950/5 dark:border-white/10 pr-6 pb-4 cursor-pointer" onclick="toggleFieldGroup(this)">
5720
+ <h3 class="text-base/7 font-semibold text-zinc-950 dark:text-white flex items-center">
5721
+ ${escapeHtml3(groupTitle)}
5722
+ <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">
5723
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
5724
+ </svg>
5725
+ </h3>
5726
+ </div>
5727
+ <div id="${groupId}-content" class="field-group-content px-6 py-6 space-y-4 ${isCollapsed ? "hidden" : ""}">
5728
+ <input type="hidden" id="${fieldId}" name="${fieldName}" value="${escapeHtml3(JSON.stringify(objectValue))}">
5729
+ <div class="space-y-4" data-structured-object-fields>
5730
+ ${subfields}
5731
+ </div>
5609
5732
  </div>
5610
5733
  </div>
5611
5734
  ${getStructuredFieldScript()}
@@ -5676,7 +5799,7 @@ function renderStructuredArrayField(field, options, baseClasses, errorClasses) {
5676
5799
  function renderStructuredArrayItem(field, itemConfig, index, itemValue, pluginStatuses, arrayItemTitle) {
5677
5800
  const itemFields = renderStructuredItemFields(field, itemConfig, index, itemValue, pluginStatuses);
5678
5801
  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">
5802
+ <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
5803
  <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
5681
5804
  <div class="flex items-center gap-3">
5682
5805
  <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 +5812,11 @@ function renderStructuredArrayItem(field, itemConfig, index, itemValue, pluginSt
5689
5812
  </div>
5690
5813
  </div>
5691
5814
  <div class="flex flex-wrap gap-2 text-xs">
5815
+ <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">
5816
+ <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>
5817
+ <path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7"></path>
5818
+ </svg>
5819
+ </button>
5692
5820
  <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
5821
  <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="4">
5694
5822
  <path stroke-linecap="round" stroke-linejoin="round" d="M12 6l-4 4m4-4l4 4m-4-4v12"/>
@@ -5707,7 +5835,7 @@ function renderStructuredArrayItem(field, itemConfig, index, itemValue, pluginSt
5707
5835
  </button>
5708
5836
  </div>
5709
5837
  </div>
5710
- <div class="mt-4 space-y-4" data-array-item-fields>
5838
+ <div class="mt-4 space-y-4 hidden" data-array-item-fields>
5711
5839
  ${itemFields}
5712
5840
  </div>
5713
5841
  </div>
@@ -5817,7 +5945,7 @@ function normalizeBlocksValue(value, discriminator) {
5817
5945
  function renderBlockTemplate(field, block, discriminator, pluginStatuses) {
5818
5946
  return `
5819
5947
  <template data-block-template="${escapeHtml3(block.name)}">
5820
- ${renderBlockCard(field, block, discriminator, "__INDEX__", {}, pluginStatuses)}
5948
+ ${renderBlockCard(field, block, discriminator, BLOCK_INDEX_TOKEN, {}, pluginStatuses)}
5821
5949
  </template>
5822
5950
  `;
5823
5951
  }
@@ -5859,7 +5987,7 @@ function renderBlockCard(field, block, discriminator, index, data, pluginStatuse
5859
5987
  `;
5860
5988
  }).join("");
5861
5989
  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">
5990
+ <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
5991
  <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
5864
5992
  <div class="flex items-start gap-3">
5865
5993
  <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 +5995,7 @@ function renderBlockCard(field, block, discriminator, index, data, pluginStatuse
5867
5995
  <path stroke-linecap="round" stroke-linejoin="round" d="M4 8h16M4 16h16"/>
5868
5996
  </svg>
5869
5997
  </div>
5870
- <div>
5998
+ <div class="cursor-pointer" data-action="toggle-block">
5871
5999
  <div class="text-sm font-semibold text-zinc-900 dark:text-white">
5872
6000
  ${escapeHtml3(block.label)}
5873
6001
  <span class="ml-2 text-xs font-normal text-zinc-500 dark:text-zinc-400" data-block-order-label></span>
@@ -5876,6 +6004,11 @@ function renderBlockCard(field, block, discriminator, index, data, pluginStatuse
5876
6004
  </div>
5877
6005
  </div>
5878
6006
  <div class="flex flex-wrap gap-2 text-xs">
6007
+ <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">
6008
+ <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>
6009
+ <path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7"></path>
6010
+ </svg>
6011
+ </button>
5879
6012
  <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
6013
  <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="4">
5881
6014
  <path stroke-linecap="round" stroke-linejoin="round" d="M12 6l-4 4m4-4l4 4m-4-4v12"/>
@@ -5894,7 +6027,7 @@ function renderBlockCard(field, block, discriminator, index, data, pluginStatuse
5894
6027
  </button>
5895
6028
  </div>
5896
6029
  </div>
5897
- <div class="mt-4 space-y-4">
6030
+ <div class="mt-4 space-y-4 hidden" data-block-content>
5898
6031
  ${blockFields}
5899
6032
  </div>
5900
6033
  </div>
@@ -5928,9 +6061,101 @@ function getStructuredFieldScript() {
5928
6061
 
5929
6062
  function initializeStructuredFields() {
5930
6063
  const readFieldValue = window.sonicReadFieldValue;
6064
+ const getDirectChild = (parent, selector) => {
6065
+ if (!(parent instanceof Element)) return null;
6066
+ return Array.from(parent.children).find(
6067
+ (child) => child instanceof Element && child.matches(selector),
6068
+ ) || null;
6069
+ };
6070
+ const getDirectStructuredSubfields = (host) =>
6071
+ Array.from(host.children).filter(
6072
+ (child) => child instanceof Element && child.classList.contains('structured-subfield'),
6073
+ );
6074
+ const getStructuredValueHost = (container) => {
6075
+ const directObjectHost = getDirectChild(container, '[data-structured-object-fields]');
6076
+ if (directObjectHost) return directObjectHost;
6077
+ const groupContent = getDirectChild(container, '.field-group-content');
6078
+ const nestedObjectHost = groupContent
6079
+ ? getDirectChild(groupContent, '[data-structured-object-fields]')
6080
+ : null;
6081
+ if (nestedObjectHost) return nestedObjectHost;
6082
+ return getDirectChild(container, '[data-array-item-fields]') || container;
6083
+ };
6084
+ const getCollectionScope = () => {
6085
+ const url = new URL(window.location.href);
6086
+ const collectionFromQuery = url.searchParams.get('collection');
6087
+ const form = document.getElementById('content-form');
6088
+ const collectionInput = form?.querySelector('input[name="collection_id"]');
6089
+ const collectionFromForm = collectionInput instanceof HTMLInputElement ? collectionInput.value : '';
6090
+ const collectionId = collectionFromQuery || collectionFromForm || '';
6091
+ return window.location.pathname + ':' + collectionId;
6092
+ };
6093
+
6094
+ const getArrayStateKey = (container) => {
6095
+ const fieldName = container.dataset.fieldName || 'unknown';
6096
+ return 'sonic:ui:repeaters:' + getCollectionScope() + ':' + fieldName;
6097
+ };
6098
+
6099
+ const readArrayState = (container) => {
6100
+ try {
6101
+ const raw = sessionStorage.getItem(getArrayStateKey(container));
6102
+ if (!raw) return null;
6103
+ const parsed = JSON.parse(raw);
6104
+ return Array.isArray(parsed) ? parsed : null;
6105
+ } catch {
6106
+ return null;
6107
+ }
6108
+ };
6109
+
6110
+ const writeArrayState = (container, state) => {
6111
+ try {
6112
+ sessionStorage.setItem(getArrayStateKey(container), JSON.stringify(state));
6113
+ } catch {}
6114
+ };
6115
+
6116
+ const setArrayItemExpanded = (item, isExpanded) => {
6117
+ const content = item.querySelector('[data-array-item-fields]');
6118
+ const icon = item.querySelector('[data-item-toggle-icon]');
6119
+ if (content instanceof HTMLElement) {
6120
+ content.classList.toggle('hidden', !isExpanded);
6121
+ }
6122
+ if (icon instanceof Element) {
6123
+ icon.classList.toggle('-rotate-90', !isExpanded);
6124
+ }
6125
+ };
6126
+
6127
+ const getArrayItems = (container, list) => {
6128
+ if (list) {
6129
+ return Array.from(list.querySelectorAll(':scope > .structured-array-item'));
6130
+ }
6131
+ return Array.from(
6132
+ container.querySelectorAll(':scope > [data-structured-array-list] > .structured-array-item'),
6133
+ );
6134
+ };
6135
+
6136
+ const captureArrayState = (container) => {
6137
+ return getArrayItems(container).map((item) => {
6138
+ const content = item.querySelector('[data-array-item-fields]');
6139
+ return content instanceof HTMLElement ? !content.classList.contains('hidden') : false;
6140
+ });
6141
+ };
6142
+
6143
+ const applyArrayState = (container, state) => {
6144
+ const items = getArrayItems(container);
6145
+ items.forEach((item, index) => {
6146
+ if (typeof state[index] === 'boolean') {
6147
+ setArrayItemExpanded(item, state[index]);
6148
+ }
6149
+ });
6150
+ };
6151
+
6152
+ const syncArrayState = (container) => {
6153
+ writeArrayState(container, captureArrayState(container));
6154
+ };
5931
6155
 
5932
6156
  const readStructuredValue = (container) => {
5933
- const fields = Array.from(container.querySelectorAll('.structured-subfield'));
6157
+ const fieldHost = getStructuredValueHost(container);
6158
+ const fields = getDirectStructuredSubfields(fieldHost);
5934
6159
  if (fields.length === 1 && fields[0].dataset.structuredField === '__value') {
5935
6160
  return readFieldValue(fields[0]);
5936
6161
  }
@@ -5944,111 +6169,229 @@ function getStructuredFieldScript() {
5944
6169
  };
5945
6170
 
5946
6171
  document.querySelectorAll('[data-structured-object]').forEach((container) => {
6172
+ if (container.closest('template')) {
6173
+ return;
6174
+ }
5947
6175
  if (container.dataset.structuredInitialized === 'true') {
5948
6176
  return;
5949
6177
  }
5950
- container.dataset.structuredInitialized = 'true';
5951
- const hiddenInput = container.querySelector('input[type="hidden"]');
6178
+ if (container.dataset.structuredInitializing === 'true') {
6179
+ return;
6180
+ }
6181
+ container.dataset.structuredInitializing = 'true';
6182
+ try {
6183
+ const hiddenInput = container.querySelector('input[type="hidden"]');
5952
6184
 
5953
- const updateHiddenInput = () => {
5954
- if (!hiddenInput) return;
5955
- const value = readStructuredValue(container);
5956
- hiddenInput.value = JSON.stringify(value);
5957
- };
6185
+ const updateHiddenInput = () => {
6186
+ if (!hiddenInput) return;
6187
+ const value = readStructuredValue(container);
6188
+ hiddenInput.value = JSON.stringify(value);
6189
+ };
5958
6190
 
5959
- container.addEventListener('input', updateHiddenInput);
5960
- container.addEventListener('change', updateHiddenInput);
5961
- updateHiddenInput();
6191
+ container.addEventListener('input', updateHiddenInput);
6192
+ container.addEventListener('change', updateHiddenInput);
6193
+ updateHiddenInput();
6194
+ container.dataset.structuredInitialized = 'true';
6195
+ } catch (error) {
6196
+ delete container.dataset.structuredInitialized;
6197
+ console.error('[structured-object] initialization failed', error);
6198
+ } finally {
6199
+ delete container.dataset.structuredInitializing;
6200
+ }
5962
6201
  });
5963
6202
 
5964
6203
  document.querySelectorAll('[data-structured-array]').forEach((container) => {
6204
+ if (container.closest('template')) {
6205
+ return;
6206
+ }
5965
6207
  if (container.dataset.structuredInitialized === 'true') {
5966
6208
  return;
5967
6209
  }
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]');
6210
+ if (container.dataset.structuredInitializing === 'true') {
6211
+ return;
6212
+ }
6213
+ container.dataset.structuredInitializing = 'true';
6214
+ try {
6215
+ const list = container.querySelector(':scope > [data-structured-array-list]');
6216
+ const hiddenInput = container.querySelector(':scope > input[type="hidden"]');
6217
+ const template = container.querySelector(':scope > template[data-structured-array-template]');
6218
+ if (
6219
+ template instanceof HTMLTemplateElement &&
6220
+ typeof template.innerHTML === 'string' &&
6221
+ template.innerHTML.trim()
6222
+ ) {
6223
+ container.__sonicStructuredArrayTemplate = template.innerHTML;
6224
+ }
5972
6225
 
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);
6226
+ const getLiveList = () =>
6227
+ list || container.querySelector(':scope > [data-structured-array-list]');
6228
+ const getLiveHiddenInput = () =>
6229
+ hiddenInput || container.querySelector(':scope > input[type="hidden"]');
6230
+ const getTemplateHtml = () => {
6231
+ if (typeof container.__sonicStructuredArrayTemplate === 'string' &&
6232
+ container.__sonicStructuredArrayTemplate.trim()) {
6233
+ return container.__sonicStructuredArrayTemplate;
5979
6234
  }
5980
6235
 
5981
- const moveUpButton = item.querySelector('[data-action="move-up"]');
5982
- if (moveUpButton instanceof HTMLButtonElement) {
5983
- moveUpButton.disabled = index === 0;
6236
+ const liveTemplate =
6237
+ template instanceof HTMLTemplateElement
6238
+ ? template
6239
+ : container.querySelector(':scope > template[data-structured-array-template]');
6240
+ if (
6241
+ liveTemplate instanceof HTMLTemplateElement &&
6242
+ typeof liveTemplate.innerHTML === 'string' &&
6243
+ liveTemplate.innerHTML.trim()
6244
+ ) {
6245
+ container.__sonicStructuredArrayTemplate = liveTemplate.innerHTML;
6246
+ return liveTemplate.innerHTML;
5984
6247
  }
6248
+ return typeof container.__sonicStructuredArrayTemplate === 'string'
6249
+ ? container.__sonicStructuredArrayTemplate
6250
+ : '';
6251
+ };
5985
6252
 
5986
- const moveDownButton = item.querySelector('[data-action="move-down"]');
5987
- if (moveDownButton instanceof HTMLButtonElement) {
5988
- moveDownButton.disabled = index === items.length - 1;
6253
+ const updateOrderLabels = () => {
6254
+ const liveList = getLiveList();
6255
+ if (!liveList) return;
6256
+ const items = getArrayItems(container, liveList);
6257
+ items.forEach((item, index) => {
6258
+ const label = item.querySelector('[data-array-order-label]');
6259
+ if (label) {
6260
+ label.textContent = '#'+ (index + 1);
6261
+ }
6262
+
6263
+ const moveUpButton = item.querySelector('[data-action="move-up"]');
6264
+ if (moveUpButton instanceof HTMLButtonElement) {
6265
+ moveUpButton.disabled = index === 0;
6266
+ }
6267
+
6268
+ const moveDownButton = item.querySelector('[data-action="move-down"]');
6269
+ if (moveDownButton instanceof HTMLButtonElement) {
6270
+ moveDownButton.disabled = index === items.length - 1;
6271
+ }
6272
+ });
6273
+ };
6274
+
6275
+ const updateHiddenInput = () => {
6276
+ const liveHiddenInput = getLiveHiddenInput();
6277
+ const liveList = getLiveList();
6278
+ if (!liveHiddenInput || !liveList) return;
6279
+ const items = getArrayItems(container, liveList);
6280
+ const values = items.map((item) => readStructuredValue(item));
6281
+ liveHiddenInput.value = JSON.stringify(values);
6282
+ // Notify parent structured containers after non-input actions (add/remove/move)
6283
+ // so nested array mutations are persisted correctly.
6284
+ liveHiddenInput.dispatchEvent(new Event('change', { bubbles: true }));
6285
+
6286
+ const emptyState = liveList.querySelector(':scope > [data-structured-empty]');
6287
+ if (emptyState) {
6288
+ emptyState.style.display = values.length === 0 ? 'block' : 'none';
5989
6289
  }
5990
- });
5991
- };
6290
+ updateOrderLabels();
6291
+ };
5992
6292
 
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);
6293
+ const addArrayItem = () => {
6294
+ const liveList = getLiveList();
6295
+ if (!liveList) return;
6296
+ const templateHtml = getTemplateHtml();
6297
+ if (!templateHtml) return;
6298
+ try {
6299
+ const nextIndex = getArrayItems(container, liveList).length;
6300
+ const html = templateHtml.replace(/__INDEX__/g, String(nextIndex));
6301
+ liveList.insertAdjacentHTML('beforeend', html);
6302
+ const newItem = liveList.lastElementChild;
6303
+ if (newItem instanceof HTMLElement) {
6304
+ // Ensure cloned template content can be initialized even if stale
6305
+ // data-structured-initialized attributes were copied.
6306
+ newItem
6307
+ .querySelectorAll('[data-structured-object], [data-structured-array]')
6308
+ .forEach((nestedContainer) => {
6309
+ if (nestedContainer instanceof HTMLElement) {
6310
+ delete nestedContainer.dataset.structuredInitialized;
6311
+ }
6312
+ });
6313
+ setArrayItemExpanded(newItem, true);
6314
+ }
6315
+ if (typeof initializeTinyMCE === 'function') {
6316
+ initializeTinyMCE();
6317
+ }
6318
+ if (typeof window.initializeQuillEditors === 'function') {
6319
+ window.initializeQuillEditors();
6320
+ }
6321
+ if (typeof initializeMDXEditor === 'function') {
6322
+ initializeMDXEditor();
6323
+ }
6324
+ if (typeof window.initializeStructuredFields === 'function') {
6325
+ window.initializeStructuredFields();
6326
+ }
6327
+ updateHiddenInput();
6328
+ syncArrayState(container);
6329
+ } catch (error) {
6330
+ console.error('[structured-array] add-item failed', error);
6331
+ }
6332
+ };
5998
6333
 
5999
- const emptyState = list.querySelector('[data-structured-empty]');
6000
- if (emptyState) {
6001
- emptyState.style.display = values.length === 0 ? 'block' : 'none';
6334
+ const topLevelAddButton = container.querySelector(
6335
+ ':scope > .flex.items-center.justify-between.gap-3 [data-action="add-item"]',
6336
+ );
6337
+ if (topLevelAddButton instanceof HTMLElement) {
6338
+ topLevelAddButton.addEventListener('click', (event) => {
6339
+ event.preventDefault();
6340
+ event.stopPropagation();
6341
+ addArrayItem();
6342
+ });
6002
6343
  }
6003
- updateOrderLabels();
6004
- };
6005
6344
 
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
- }
6345
+ const dragList = getLiveList();
6346
+ if (typeof window.initializeDragSortable === 'function' && dragList) {
6347
+ window.initializeDragSortable(dragList, {
6348
+ itemSelector: '.structured-array-item',
6349
+ handleSelector: '[data-action="drag-handle"]',
6350
+ onUpdate: () => {
6351
+ updateHiddenInput();
6352
+ syncArrayState(container);
6353
+ }
6354
+ });
6355
+ }
6013
6356
 
6014
- container.addEventListener('click', (event) => {
6357
+ container.addEventListener('click', (event) => {
6015
6358
  const target = event.target;
6016
6359
  if (!(target instanceof Element)) return;
6017
6360
  const actionButton = target.closest('[data-action]');
6018
6361
  if (!actionButton || actionButton.hasAttribute('disabled')) return;
6362
+ const actionOwner = actionButton.closest('[data-structured-array]');
6363
+ if (actionOwner !== container) return;
6019
6364
 
6020
- const action = actionButton.getAttribute('data-action');
6365
+ const action = actionButton.getAttribute('data-action');
6021
6366
 
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();
6367
+ if (action === 'add-item') {
6368
+ addArrayItem();
6369
+ return;
6035
6370
  }
6036
- updateHiddenInput();
6037
- return;
6038
- }
6039
6371
 
6040
6372
  const item = actionButton.closest('.structured-array-item');
6041
- if (!item || !list) return;
6373
+ const liveList = getLiveList();
6374
+ if (!item || !liveList) return;
6375
+
6376
+ if (action === 'toggle-item') {
6377
+ const content = item.querySelector('[data-array-item-fields]');
6378
+ if (!(content instanceof HTMLElement)) return;
6379
+ setArrayItemExpanded(item, content.classList.contains('hidden'));
6380
+ syncArrayState(container);
6381
+ return;
6382
+ }
6042
6383
 
6043
6384
  if (action === 'remove-item') {
6044
6385
  if (typeof requestRepeaterDelete === 'function') {
6045
6386
  requestRepeaterDelete(() => {
6046
6387
  item.remove();
6047
6388
  updateHiddenInput();
6389
+ syncArrayState(container);
6048
6390
  });
6049
6391
  } else {
6050
6392
  item.remove();
6051
6393
  updateHiddenInput();
6394
+ syncArrayState(container);
6052
6395
  }
6053
6396
  return;
6054
6397
  }
@@ -6056,8 +6399,9 @@ function getStructuredFieldScript() {
6056
6399
  if (action === 'move-up') {
6057
6400
  const previous = item.previousElementSibling;
6058
6401
  if (previous) {
6059
- list.insertBefore(item, previous);
6402
+ liveList.insertBefore(item, previous);
6060
6403
  updateHiddenInput();
6404
+ syncArrayState(container);
6061
6405
  }
6062
6406
  return;
6063
6407
  }
@@ -6065,29 +6409,43 @@ function getStructuredFieldScript() {
6065
6409
  if (action === 'move-down') {
6066
6410
  const next = item.nextElementSibling;
6067
6411
  if (next) {
6068
- list.insertBefore(next, item);
6412
+ liveList.insertBefore(next, item);
6069
6413
  updateHiddenInput();
6414
+ syncArrayState(container);
6070
6415
  }
6071
6416
  }
6072
- });
6417
+ });
6073
6418
 
6074
- container.addEventListener('input', (event) => {
6419
+ container.addEventListener('input', (event) => {
6075
6420
  const target = event.target;
6076
6421
  if (!(target instanceof Element)) return;
6077
6422
  if (target.closest('[data-structured-array-list]')) {
6078
6423
  updateHiddenInput();
6079
6424
  }
6080
- });
6425
+ });
6081
6426
 
6082
- container.addEventListener('change', (event) => {
6427
+ container.addEventListener('change', (event) => {
6083
6428
  const target = event.target;
6084
6429
  if (!(target instanceof Element)) return;
6085
6430
  if (target.closest('[data-structured-array-list]')) {
6086
6431
  updateHiddenInput();
6087
6432
  }
6088
- });
6433
+ });
6089
6434
 
6090
- updateHiddenInput();
6435
+ updateHiddenInput();
6436
+ const savedArrayState = readArrayState(container);
6437
+ if (savedArrayState) {
6438
+ applyArrayState(container, savedArrayState);
6439
+ } else {
6440
+ syncArrayState(container);
6441
+ }
6442
+ container.dataset.structuredInitialized = 'true';
6443
+ } catch (error) {
6444
+ delete container.dataset.structuredInitialized;
6445
+ console.error('[structured-array] initialization failed', error);
6446
+ } finally {
6447
+ delete container.dataset.structuredInitializing;
6448
+ }
6091
6449
  });
6092
6450
  }
6093
6451
 
@@ -6102,7 +6460,10 @@ function getStructuredFieldScript() {
6102
6460
  document.addEventListener('htmx:afterSwap', function() {
6103
6461
  setTimeout(initializeStructuredFields, 50);
6104
6462
  });
6105
- } else if (typeof window.initializeStructuredFields === 'function') {
6463
+ } else if (
6464
+ typeof window.initializeStructuredFields === 'function' &&
6465
+ document.readyState !== 'loading'
6466
+ ) {
6106
6467
  window.initializeStructuredFields();
6107
6468
  }
6108
6469
  </script>
@@ -6114,6 +6475,68 @@ function getBlocksFieldScript() {
6114
6475
  <script>
6115
6476
  if (!window.__sonicBlocksFieldInit) {
6116
6477
  window.__sonicBlocksFieldInit = true;
6478
+ const getCollectionScope = () => {
6479
+ const url = new URL(window.location.href);
6480
+ const collectionFromQuery = url.searchParams.get('collection');
6481
+ const form = document.getElementById('content-form');
6482
+ const collectionInput = form?.querySelector('input[name="collection_id"]');
6483
+ const collectionFromForm = collectionInput instanceof HTMLInputElement ? collectionInput.value : '';
6484
+ const collectionId = collectionFromQuery || collectionFromForm || '';
6485
+ return window.location.pathname + ':' + collectionId;
6486
+ };
6487
+
6488
+ const getBlocksStateKey = (container) => {
6489
+ const fieldName = container.dataset.fieldName || 'unknown';
6490
+ return 'sonic:ui:blocks:' + getCollectionScope() + ':' + fieldName;
6491
+ };
6492
+
6493
+ const readBlocksState = (container) => {
6494
+ try {
6495
+ const raw = sessionStorage.getItem(getBlocksStateKey(container));
6496
+ if (!raw) return null;
6497
+ const parsed = JSON.parse(raw);
6498
+ return Array.isArray(parsed) ? parsed : null;
6499
+ } catch {
6500
+ return null;
6501
+ }
6502
+ };
6503
+
6504
+ const writeBlocksState = (container, state) => {
6505
+ try {
6506
+ sessionStorage.setItem(getBlocksStateKey(container), JSON.stringify(state));
6507
+ } catch {}
6508
+ };
6509
+
6510
+ const setBlockExpanded = (item, isExpanded) => {
6511
+ const content = item.querySelector('[data-block-content]');
6512
+ const icon = item.querySelector('[data-block-toggle-icon]');
6513
+ if (content instanceof HTMLElement) {
6514
+ content.classList.toggle('hidden', !isExpanded);
6515
+ }
6516
+ if (icon instanceof Element) {
6517
+ icon.classList.toggle('-rotate-90', !isExpanded);
6518
+ }
6519
+ };
6520
+
6521
+ const captureBlocksState = (container) => {
6522
+ return Array.from(container.querySelectorAll('.blocks-item')).map((item) => {
6523
+ const content = item.querySelector('[data-block-content]');
6524
+ return content instanceof HTMLElement ? !content.classList.contains('hidden') : false;
6525
+ });
6526
+ };
6527
+
6528
+ const applyBlocksState = (container, state) => {
6529
+ const items = Array.from(container.querySelectorAll('.blocks-item'));
6530
+ items.forEach((item, index) => {
6531
+ if (typeof state[index] === 'boolean') {
6532
+ setBlockExpanded(item, state[index]);
6533
+ }
6534
+ });
6535
+ };
6536
+
6537
+ const syncBlocksState = (container) => {
6538
+ writeBlocksState(container, captureBlocksState(container));
6539
+ };
6117
6540
 
6118
6541
  function initializeBlocksFields() {
6119
6542
  document.querySelectorAll('.blocks-field').forEach((container) => {
@@ -6201,7 +6624,10 @@ function getBlocksFieldScript() {
6201
6624
  window.initializeDragSortable(list, {
6202
6625
  itemSelector: '.blocks-item',
6203
6626
  handleSelector: '[data-action="drag-handle"]',
6204
- onUpdate: updateHiddenInput
6627
+ onUpdate: () => {
6628
+ updateHiddenInput();
6629
+ syncBlocksState(container);
6630
+ }
6205
6631
  });
6206
6632
  }
6207
6633
 
@@ -6223,8 +6649,12 @@ function getBlocksFieldScript() {
6223
6649
  if (!template) return;
6224
6650
 
6225
6651
  const nextIndex = list.querySelectorAll('.blocks-item').length;
6226
- const html = template.innerHTML.replace(/__INDEX__/g, String(nextIndex));
6652
+ const html = template.innerHTML.replace(/__BLOCK_INDEX__/g, String(nextIndex));
6227
6653
  list.insertAdjacentHTML('beforeend', html);
6654
+ const newItem = list.lastElementChild;
6655
+ if (newItem instanceof HTMLElement) {
6656
+ setBlockExpanded(newItem, true);
6657
+ }
6228
6658
  if (typeSelect) {
6229
6659
  typeSelect.value = '';
6230
6660
  }
@@ -6233,21 +6663,32 @@ function getBlocksFieldScript() {
6233
6663
  window.initializeStructuredFields();
6234
6664
  }
6235
6665
  updateHiddenInput();
6666
+ syncBlocksState(container);
6236
6667
  return;
6237
6668
  }
6238
6669
 
6239
6670
  const item = actionButton.closest('.blocks-item');
6240
6671
  if (!item || !list) return;
6241
6672
 
6673
+ if (action === 'toggle-block') {
6674
+ const content = item.querySelector('[data-block-content]');
6675
+ if (!(content instanceof HTMLElement)) return;
6676
+ setBlockExpanded(item, content.classList.contains('hidden'));
6677
+ syncBlocksState(container);
6678
+ return;
6679
+ }
6680
+
6242
6681
  if (action === 'remove-block') {
6243
6682
  if (typeof requestRepeaterDelete === 'function') {
6244
6683
  requestRepeaterDelete(() => {
6245
6684
  item.remove();
6246
6685
  updateHiddenInput();
6686
+ syncBlocksState(container);
6247
6687
  }, 'block');
6248
6688
  } else {
6249
6689
  item.remove();
6250
6690
  updateHiddenInput();
6691
+ syncBlocksState(container);
6251
6692
  }
6252
6693
  return;
6253
6694
  }
@@ -6257,6 +6698,7 @@ function getBlocksFieldScript() {
6257
6698
  if (previous) {
6258
6699
  list.insertBefore(item, previous);
6259
6700
  updateHiddenInput();
6701
+ syncBlocksState(container);
6260
6702
  }
6261
6703
  return;
6262
6704
  }
@@ -6266,6 +6708,7 @@ function getBlocksFieldScript() {
6266
6708
  if (next) {
6267
6709
  list.insertBefore(next, item);
6268
6710
  updateHiddenInput();
6711
+ syncBlocksState(container);
6269
6712
  }
6270
6713
  }
6271
6714
  });
@@ -6287,6 +6730,12 @@ function getBlocksFieldScript() {
6287
6730
  });
6288
6731
 
6289
6732
  updateHiddenInput();
6733
+ const savedBlocksState = readBlocksState(container);
6734
+ if (savedBlocksState) {
6735
+ applyBlocksState(container, savedBlocksState);
6736
+ } else {
6737
+ syncBlocksState(container);
6738
+ }
6290
6739
  });
6291
6740
  }
6292
6741
 
@@ -6323,6 +6772,7 @@ chunkLTKV7AE5_cjs.init_admin_layout_catalyst_template();
6323
6772
  function renderContentFormPage(data) {
6324
6773
  const isEdit = data.isEdit || !!data.id;
6325
6774
  const title = isEdit ? `Edit: ${data.title || "Content"}` : `New ${data.collection.display_name}`;
6775
+ const hasValidationErrors = Boolean(data.validationErrors && Object.keys(data.validationErrors).length > 0);
6326
6776
  const backUrl = data.referrerParams ? `/admin/content?${data.referrerParams}` : `/admin/content?collection=${data.collection.id}`;
6327
6777
  const coreFields = data.fields.filter((f) => ["title", "slug", "content"].includes(f.field_name));
6328
6778
  const contentFields = data.fields.filter((f) => !["title", "slug", "content"].includes(f.field_name) && !f.field_name.startsWith("meta_"));
@@ -6411,6 +6861,7 @@ function renderContentFormPage(data) {
6411
6861
  ${isEdit ? `hx-put="/admin/content/${data.id}"` : `hx-post="/admin/content"`}
6412
6862
  hx-target="#form-messages"
6413
6863
  hx-encoding="multipart/form-data"
6864
+ data-has-validation-errors="${hasValidationErrors ? "true" : "false"}"
6414
6865
  class="space-y-6"
6415
6866
  >
6416
6867
  <input type="hidden" name="collection_id" value="${data.collection.id}">
@@ -6691,39 +7142,456 @@ function renderContentFormPage(data) {
6691
7142
 
6692
7143
  <!-- Dynamic Field Scripts -->
6693
7144
  <script>
7145
+ const contentFormCollectionId = ${JSON.stringify(data.collection.id)};
7146
+
7147
+ function getFieldGroupScope() {
7148
+ const url = new URL(window.location.href);
7149
+ const urlCollectionId = url.searchParams.get('collection');
7150
+ const effectiveCollectionId = urlCollectionId || contentFormCollectionId || '';
7151
+ return window.location.pathname + ':' + effectiveCollectionId;
7152
+ }
7153
+
7154
+ function getItemPosition(itemSelector, item) {
7155
+ if (!(item instanceof Element)) return -1;
7156
+ const parent = item.parentElement;
7157
+ if (!parent) return -1;
7158
+ return Array.from(parent.querySelectorAll(':scope > ' + itemSelector)).indexOf(item);
7159
+ }
7160
+
7161
+ function stripIndexedFieldPrefix(fullFieldName, prefix) {
7162
+ if (!fullFieldName || !prefix || !fullFieldName.startsWith(prefix)) {
7163
+ return fullFieldName;
7164
+ }
7165
+
7166
+ const remainder = fullFieldName.slice(prefix.length);
7167
+ const indexMatch = remainder.match(/^(\\d+)(-|__)(.*)$/);
7168
+ if (!indexMatch) {
7169
+ return fullFieldName;
7170
+ }
7171
+
7172
+ return indexMatch[3];
7173
+ }
7174
+
7175
+ function getFieldGroupStorageKey(groupOrId) {
7176
+ const defaultGroupId = typeof groupOrId === 'string' ? groupOrId : (groupOrId?.getAttribute('data-group-id') || 'unknown');
7177
+ const group = typeof groupOrId === 'string'
7178
+ ? document.querySelector('.field-group[data-group-id="' + defaultGroupId + '"]')
7179
+ : groupOrId;
7180
+
7181
+ const scopePrefix = 'sonic:ui:objects:' + getFieldGroupScope() + ':';
7182
+ if (!(group instanceof Element)) {
7183
+ return scopePrefix + defaultGroupId;
7184
+ }
7185
+
7186
+ const fullFieldName = group.getAttribute('data-field-name') || '';
7187
+
7188
+ const blocksField = group.closest('.blocks-field');
7189
+ const blockItem = group.closest('.blocks-item');
7190
+ if (blocksField instanceof Element && blockItem instanceof Element) {
7191
+ const blocksFieldName = blocksField.getAttribute('data-field-name') || 'unknown';
7192
+ const blockPosition = getItemPosition('.blocks-item', blockItem);
7193
+ const relativePath = stripIndexedFieldPrefix(fullFieldName, 'block-' + blocksFieldName + '-') || defaultGroupId;
7194
+ return scopePrefix + 'blocks:' + blocksFieldName + ':' + blockPosition + ':' + relativePath;
7195
+ }
7196
+
7197
+ const arrayField = group.closest('[data-structured-array][data-field-name]');
7198
+ const arrayItem = group.closest('.structured-array-item');
7199
+ if (arrayField instanceof Element && arrayItem instanceof Element) {
7200
+ const arrayFieldName = arrayField.getAttribute('data-field-name') || 'unknown';
7201
+ const itemPosition = getItemPosition('.structured-array-item', arrayItem);
7202
+ const relativePath = stripIndexedFieldPrefix(fullFieldName, 'array-' + arrayFieldName + '-') || defaultGroupId;
7203
+ return scopePrefix + 'repeaters:' + arrayFieldName + ':' + itemPosition + ':' + relativePath;
7204
+ }
7205
+
7206
+ return scopePrefix + defaultGroupId;
7207
+ }
7208
+
7209
+ function loadFieldGroupState(group) {
7210
+ try {
7211
+ const value = sessionStorage.getItem(getFieldGroupStorageKey(group));
7212
+ if (value === '1') return true;
7213
+ if (value === '0') return false;
7214
+ } catch {}
7215
+ return null;
7216
+ }
7217
+
7218
+ function saveFieldGroupState(group, isCollapsed) {
7219
+ try {
7220
+ sessionStorage.setItem(getFieldGroupStorageKey(group), isCollapsed ? '1' : '0');
7221
+ } catch {}
7222
+ }
7223
+
7224
+ function resolveFieldGroupElements(groupOrId) {
7225
+ let group = null;
7226
+
7227
+ if (groupOrId instanceof Element) {
7228
+ group = groupOrId.classList.contains('field-group')
7229
+ ? groupOrId
7230
+ : groupOrId.closest('.field-group[data-group-id]');
7231
+ } else if (typeof groupOrId === 'string' && groupOrId) {
7232
+ group = document.querySelector('.field-group[data-group-id="' + groupOrId + '"]');
7233
+ }
7234
+
7235
+ let content = null;
7236
+ let icon = null;
7237
+
7238
+ if (group instanceof Element) {
7239
+ content = group.querySelector(':scope > .field-group-content');
7240
+ icon = group.querySelector(':scope > .field-group-header svg[id$="-icon"]');
7241
+ }
7242
+
7243
+ // Legacy fallback for any existing calls still passing string IDs.
7244
+ if (!(content instanceof HTMLElement) && typeof groupOrId === 'string') {
7245
+ content = document.getElementById(groupOrId + '-content');
7246
+ }
7247
+ if (!(icon instanceof Element) && typeof groupOrId === 'string') {
7248
+ icon = document.getElementById(groupOrId + '-icon');
7249
+ }
7250
+
7251
+ if (!(group instanceof Element) && content instanceof Element) {
7252
+ group = content.closest('.field-group[data-group-id]');
7253
+ }
7254
+
7255
+ return { group, content, icon };
7256
+ }
7257
+
7258
+ function applyFieldGroupState(groupOrId, isCollapsed) {
7259
+ const { content, icon } = resolveFieldGroupElements(groupOrId);
7260
+ if (!(content instanceof HTMLElement) || !(icon instanceof Element)) return;
7261
+ content.classList.toggle('hidden', isCollapsed);
7262
+ icon.classList.toggle('-rotate-90', isCollapsed);
7263
+ }
7264
+
7265
+ function restoreFieldGroupStates() {
7266
+ document.querySelectorAll('.field-group[data-group-id]').forEach((group) => {
7267
+ const savedState = loadFieldGroupState(group);
7268
+ if (savedState === null) return;
7269
+ applyFieldGroupState(group, savedState);
7270
+ });
7271
+ }
7272
+
7273
+ function persistAllFieldGroupStates() {
7274
+ document.querySelectorAll('.field-group[data-group-id]').forEach((group) => {
7275
+ const { content } = resolveFieldGroupElements(group);
7276
+ if (!(content instanceof HTMLElement)) return;
7277
+ saveFieldGroupState(group, content.classList.contains('hidden'));
7278
+ });
7279
+ }
7280
+
7281
+ function setValidationHeaderIndicator(container) {
7282
+ if (!(container instanceof Element)) return;
7283
+ let header = null;
7284
+ let markerTarget = null;
7285
+
7286
+ if (container.classList.contains('field-group')) {
7287
+ header = container.querySelector(':scope > .field-group-header');
7288
+ markerTarget = container.querySelector(':scope > .field-group-header h3');
7289
+ } else if (container.classList.contains('structured-array-item')) {
7290
+ header = container.querySelector('[data-action="toggle-item"]');
7291
+ markerTarget = header;
7292
+ } else if (container.classList.contains('blocks-item')) {
7293
+ header = container.querySelector('[data-action="toggle-block"]');
7294
+ markerTarget = header;
7295
+ }
7296
+
7297
+ if (!(header instanceof HTMLElement)) return;
7298
+ if (!(markerTarget instanceof HTMLElement)) {
7299
+ markerTarget = header;
7300
+ }
7301
+
7302
+ header.dataset.validationHeaderError = 'true';
7303
+ header.classList.add('text-pink-700', 'dark:text-pink-300');
7304
+
7305
+ if (!markerTarget.querySelector('[data-validation-indicator]')) {
7306
+ const marker = document.createElement('span');
7307
+ marker.setAttribute('data-validation-indicator', 'true');
7308
+ marker.className = 'ml-2 inline-block h-2 w-2 rounded-full bg-pink-500 align-middle';
7309
+ marker.setAttribute('aria-hidden', 'true');
7310
+ markerTarget.appendChild(marker);
7311
+ }
7312
+ }
7313
+
7314
+ function clearValidationIndicators() {
7315
+ document.querySelectorAll('[data-validation-header-error="true"]').forEach((el) => {
7316
+ if (!(el instanceof HTMLElement)) return;
7317
+ delete el.dataset.validationHeaderError;
7318
+ el.classList.remove('text-pink-700', 'dark:text-pink-300');
7319
+ });
7320
+
7321
+ document.querySelectorAll('[data-validation-indicator]').forEach((el) => el.remove());
7322
+ }
7323
+
7324
+ function expandContainerForValidation(container) {
7325
+ if (!(container instanceof Element)) return;
7326
+
7327
+ if (container.classList.contains('field-group')) {
7328
+ applyFieldGroupState(container, false);
7329
+ return;
7330
+ }
7331
+
7332
+ if (container.classList.contains('structured-array-item')) {
7333
+ const content = container.querySelector('[data-array-item-fields]');
7334
+ const icon = container.querySelector('[data-item-toggle-icon]');
7335
+ if (content instanceof HTMLElement) {
7336
+ content.classList.remove('hidden');
7337
+ }
7338
+ if (icon instanceof Element) {
7339
+ icon.classList.remove('-rotate-90');
7340
+ }
7341
+ return;
7342
+ }
7343
+
7344
+ if (container.classList.contains('blocks-item')) {
7345
+ const content = container.querySelector('[data-block-content]');
7346
+ const icon = container.querySelector('[data-block-toggle-icon]');
7347
+ if (content instanceof HTMLElement) {
7348
+ content.classList.remove('hidden');
7349
+ }
7350
+ if (icon instanceof Element) {
7351
+ icon.classList.remove('-rotate-90');
7352
+ }
7353
+ }
7354
+ }
7355
+
7356
+ function walkErrorContainers(node, expand) {
7357
+ if (!(node instanceof Element)) return;
7358
+ const visited = new Set();
7359
+ let cursor = node;
7360
+ while (cursor) {
7361
+ const candidates = [
7362
+ cursor.closest('.structured-array-item'),
7363
+ cursor.closest('.blocks-item'),
7364
+ cursor.closest('.field-group[data-group-id]')
7365
+ ].filter((c) => c instanceof Element && !visited.has(c));
7366
+
7367
+ if (candidates.length === 0) break;
7368
+
7369
+ // Pick nearest ancestor container to preserve "first-error path only".
7370
+ let nearest = candidates[0];
7371
+ let bestDistance = Number.MAX_SAFE_INTEGER;
7372
+ for (const candidate of candidates) {
7373
+ let distance = 0;
7374
+ let walker = cursor;
7375
+ while (walker && walker !== candidate) {
7376
+ walker = walker.parentElement;
7377
+ distance += 1;
7378
+ }
7379
+ if (distance < bestDistance) {
7380
+ bestDistance = distance;
7381
+ nearest = candidate;
7382
+ }
7383
+ }
7384
+
7385
+ visited.add(nearest);
7386
+ setValidationHeaderIndicator(nearest);
7387
+ if (expand) {
7388
+ expandContainerForValidation(nearest);
7389
+ }
7390
+ cursor = nearest.parentElement;
7391
+ }
7392
+ }
7393
+
7394
+ function getFocusableTargetFromErrorGroup(group) {
7395
+ if (!(group instanceof Element)) return null;
7396
+ return (
7397
+ group.querySelector('input:not([type="hidden"]):not([disabled]), textarea:not([disabled]), select:not([disabled]), [contenteditable="true"]') ||
7398
+ group.querySelector('button:not([disabled])')
7399
+ );
7400
+ }
7401
+
7402
+ function revealServerValidationErrors() {
7403
+ clearValidationIndicators();
7404
+
7405
+ const errorGroups = Array.from(document.querySelectorAll('.form-group[data-has-errors="true"]'));
7406
+ if (errorGroups.length === 0) return;
7407
+
7408
+ // Add indicators for all errored sections, expand only first-error path.
7409
+ errorGroups.forEach((group, index) => {
7410
+ walkErrorContainers(group, index === 0);
7411
+ });
7412
+
7413
+ const firstTarget = getFocusableTargetFromErrorGroup(errorGroups[0]);
7414
+ if (firstTarget instanceof HTMLElement) {
7415
+ firstTarget.scrollIntoView({ behavior: 'smooth', block: 'center' });
7416
+ firstTarget.focus({ preventScroll: true });
7417
+ }
7418
+ }
7419
+
7420
+ function revealNativeValidationErrors(form) {
7421
+ if (!(form instanceof HTMLFormElement)) return;
7422
+ clearValidationIndicators();
7423
+
7424
+ const invalidControls = Array.from(form.querySelectorAll(':invalid'));
7425
+ if (invalidControls.length === 0) return;
7426
+
7427
+ invalidControls.forEach((control, index) => {
7428
+ walkErrorContainers(control, index === 0);
7429
+ });
7430
+
7431
+ const first = invalidControls[0];
7432
+ if (first instanceof HTMLElement) {
7433
+ first.scrollIntoView({ behavior: 'smooth', block: 'center' });
7434
+ first.focus({ preventScroll: true });
7435
+ }
7436
+ }
7437
+
6694
7438
  // 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]');
7439
+ function toggleFieldGroup(groupOrTrigger) {
7440
+ const { group, content } = resolveFieldGroupElements(groupOrTrigger);
7441
+ if (!(group instanceof Element)) return;
7442
+ if (!(content instanceof HTMLElement)) return;
7443
+
7444
+ const isCollapsed = !content.classList.contains('hidden');
7445
+ applyFieldGroupState(group, isCollapsed);
7446
+ saveFieldGroupState(group, isCollapsed);
7447
+ }
7448
+
7449
+ if (document.readyState === 'loading') {
7450
+ document.addEventListener('DOMContentLoaded', () => {
7451
+ restoreFieldGroupStates();
7452
+ const form = document.getElementById('content-form');
7453
+ if (form?.getAttribute('data-has-validation-errors') === 'true') {
7454
+ revealServerValidationErrors();
7455
+ }
7456
+ });
7457
+ } else {
7458
+ restoreFieldGroupStates();
7459
+ const form = document.getElementById('content-form');
7460
+ if (form?.getAttribute('data-has-validation-errors') === 'true') {
7461
+ revealServerValidationErrors();
6705
7462
  }
6706
7463
  }
6707
7464
 
7465
+ document.addEventListener('htmx:afterSwap', function() {
7466
+ setTimeout(() => {
7467
+ restoreFieldGroupStates();
7468
+ const form = document.getElementById('content-form');
7469
+ if (form?.getAttribute('data-has-validation-errors') === 'true') {
7470
+ revealServerValidationErrors();
7471
+ }
7472
+ }, 50);
7473
+ });
7474
+
7475
+ const contentFormEl = document.getElementById('content-form');
7476
+ if (contentFormEl instanceof HTMLFormElement) {
7477
+ contentFormEl.addEventListener('submit', () => {
7478
+ persistAllFieldGroupStates();
7479
+ }, true);
7480
+ }
7481
+
7482
+ window.addEventListener('beforeunload', () => {
7483
+ persistAllFieldGroupStates();
7484
+ });
7485
+
7486
+ document.addEventListener('visibilitychange', () => {
7487
+ if (document.visibilityState === 'hidden') {
7488
+ persistAllFieldGroupStates();
7489
+ }
7490
+ });
7491
+
7492
+ let pendingNativeValidationReveal = false;
7493
+ document.addEventListener('invalid', function(event) {
7494
+ const target = event.target;
7495
+ if (!(target instanceof Element)) return;
7496
+ const form = target.closest('form');
7497
+ if (!(form instanceof HTMLFormElement)) return;
7498
+
7499
+ if (pendingNativeValidationReveal) return;
7500
+ pendingNativeValidationReveal = true;
7501
+
7502
+ // Expand only first invalid path synchronously so the browser can focus it
7503
+ // and avoid "invalid form control is not focusable" errors.
7504
+ walkErrorContainers(target, true);
7505
+
7506
+ setTimeout(() => {
7507
+ pendingNativeValidationReveal = false;
7508
+ revealNativeValidationErrors(form);
7509
+ }, 0);
7510
+ }, true);
7511
+
6708
7512
  // Media field functions
6709
- let currentMediaFieldId = null;
7513
+ function notifyFieldChange(input) {
7514
+ if (!input) return;
7515
+ input.dispatchEvent(new Event('input', { bubbles: true }));
7516
+ input.dispatchEvent(new Event('change', { bubbles: true }));
7517
+ }
7518
+
7519
+ function getActiveMediaModal() {
7520
+ const modal = document.getElementById('media-selector-modal');
7521
+ return modal instanceof HTMLElement ? modal : null;
7522
+ }
7523
+
7524
+ function getMediaFieldElements(fieldId) {
7525
+ if (!fieldId) {
7526
+ return {
7527
+ fieldId: '',
7528
+ hiddenInput: null,
7529
+ preview: null,
7530
+ mediaField: null,
7531
+ actionsDiv: null,
7532
+ };
7533
+ }
7534
+
7535
+ const hiddenInput = document.getElementById(fieldId);
7536
+ const preview = document.getElementById(fieldId + '-preview');
7537
+ const mediaField = hiddenInput?.closest('.media-field-container') || null;
7538
+ const actionsDiv = mediaField?.querySelector('.media-actions') || null;
7539
+
7540
+ return {
7541
+ fieldId,
7542
+ hiddenInput,
7543
+ preview,
7544
+ mediaField,
7545
+ actionsDiv,
7546
+ };
7547
+ }
7548
+
7549
+ function getActiveMediaTarget() {
7550
+ const modal = getActiveMediaModal();
7551
+ const fieldId = modal?.dataset.targetFieldId || '';
7552
+ return {
7553
+ modal,
7554
+ originalValue: modal?.dataset.originalValue || '',
7555
+ ...getMediaFieldElements(fieldId),
7556
+ };
7557
+ }
7558
+
7559
+ function ensureSingleMediaRemoveButton(fieldId, actionsDiv) {
7560
+ if (!(actionsDiv instanceof HTMLElement)) return;
7561
+ const existingRemoveButton = actionsDiv.querySelector('[data-media-remove="true"]');
7562
+ if (existingRemoveButton) return;
7563
+
7564
+ const removeBtn = document.createElement('button');
7565
+ removeBtn.type = 'button';
7566
+ removeBtn.setAttribute('data-media-remove', 'true');
7567
+ removeBtn.onclick = () => clearMediaField(fieldId);
7568
+ removeBtn.className = 'inline-flex items-center px-4 py-2 bg-red-600 text-white rounded-xl hover:bg-red-700 transition-all';
7569
+ removeBtn.textContent = 'Remove';
7570
+ actionsDiv.appendChild(removeBtn);
7571
+ }
6710
7572
 
6711
7573
  function openMediaSelector(fieldId) {
6712
- currentMediaFieldId = fieldId;
7574
+ const existingModal = getActiveMediaModal();
7575
+ if (existingModal) {
7576
+ existingModal.remove();
7577
+ }
7578
+
6713
7579
  // Store the original value in case user cancels
6714
- const originalValue = document.getElementById(fieldId)?.value || '';
7580
+ const originalValue = getMediaFieldElements(fieldId).hiddenInput?.value || '';
6715
7581
 
6716
7582
  // Open media library modal
6717
7583
  const modal = document.createElement('div');
6718
7584
  modal.className = 'fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50';
6719
7585
  modal.id = 'media-selector-modal';
7586
+ modal.dataset.targetFieldId = fieldId;
7587
+ modal.dataset.originalValue = originalValue;
6720
7588
  modal.innerHTML = \`
6721
7589
  <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
7590
  <h3 class="text-lg font-semibold text-zinc-950 dark:text-white mb-4">Select Media</h3>
6723
7591
  <div id="media-grid-container" hx-get="/admin/media/selector" hx-trigger="load"></div>
6724
7592
  <div class="mt-4 flex justify-end space-x-2">
6725
7593
  <button
6726
- onclick="cancelMediaSelection('\${fieldId}', '\${originalValue}')"
7594
+ onclick="cancelMediaSelection()"
6727
7595
  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
7596
  Cancel
6729
7597
  </button>
@@ -6743,23 +7611,23 @@ function renderContentFormPage(data) {
6743
7611
  }
6744
7612
 
6745
7613
  function closeMediaSelector() {
6746
- const modal = document.getElementById('media-selector-modal');
7614
+ const modal = getActiveMediaModal();
6747
7615
  if (modal) {
6748
7616
  modal.remove();
6749
7617
  }
6750
- currentMediaFieldId = null;
6751
7618
  }
6752
7619
 
6753
- function cancelMediaSelection(fieldId, originalValue) {
7620
+ function cancelMediaSelection() {
7621
+ const { hiddenInput, preview, originalValue } = getActiveMediaTarget();
7622
+
6754
7623
  // Restore original value
6755
- const hiddenInput = document.getElementById(fieldId);
6756
7624
  if (hiddenInput) {
6757
7625
  hiddenInput.value = originalValue;
7626
+ notifyFieldChange(hiddenInput);
6758
7627
  }
6759
7628
 
6760
7629
  // If original value was empty, hide the preview and show select button
6761
7630
  if (!originalValue) {
6762
- const preview = document.getElementById(fieldId + '-preview');
6763
7631
  if (preview) {
6764
7632
  preview.classList.add('hidden');
6765
7633
  }
@@ -6770,11 +7638,11 @@ function renderContentFormPage(data) {
6770
7638
  }
6771
7639
 
6772
7640
  function clearMediaField(fieldId) {
6773
- const hiddenInput = document.getElementById(fieldId);
6774
- const preview = document.getElementById(fieldId + '-preview');
7641
+ const { hiddenInput, preview, actionsDiv } = getMediaFieldElements(fieldId);
6775
7642
 
6776
7643
  if (hiddenInput) {
6777
7644
  hiddenInput.value = '';
7645
+ notifyFieldChange(hiddenInput);
6778
7646
  }
6779
7647
 
6780
7648
  if (preview) {
@@ -6784,25 +7652,34 @@ function renderContentFormPage(data) {
6784
7652
  }
6785
7653
  preview.classList.add('hidden');
6786
7654
  }
7655
+
7656
+ const removeButton = actionsDiv?.querySelector('[data-media-remove="true"]');
7657
+ if (removeButton) {
7658
+ removeButton.remove();
7659
+ }
6787
7660
  }
6788
7661
 
6789
7662
  // Global function to remove a single media from multiple selection
6790
7663
  window.removeMediaFromMultiple = function(fieldId, urlToRemove) {
6791
- const hiddenInput = document.getElementById(fieldId);
7664
+ const { hiddenInput, preview } = getMediaFieldElements(fieldId);
6792
7665
  if (!hiddenInput) return;
6793
7666
 
6794
7667
  const values = hiddenInput.value.split(',').filter(url => url !== urlToRemove);
6795
7668
  hiddenInput.value = values.join(',');
7669
+ notifyFieldChange(hiddenInput);
6796
7670
 
6797
7671
  // Remove preview item
6798
- const previewItem = document.querySelector(\`[data-url="\${urlToRemove}"]\`);
7672
+ const previewItem =
7673
+ preview &&
7674
+ Array.from(preview.querySelectorAll('[data-url]')).find(
7675
+ (item) => item.getAttribute('data-url') === urlToRemove,
7676
+ );
6799
7677
  if (previewItem) {
6800
7678
  previewItem.remove();
6801
7679
  }
6802
7680
 
6803
7681
  // Hide preview grid if empty
6804
7682
  if (values.length === 0) {
6805
- const preview = document.getElementById(fieldId + '-preview');
6806
7683
  if (preview) {
6807
7684
  preview.classList.add('hidden');
6808
7685
  }
@@ -6811,39 +7688,24 @@ function renderContentFormPage(data) {
6811
7688
 
6812
7689
  // Global function called by media selector buttons
6813
7690
  window.selectMediaFile = function(mediaId, mediaUrl, filename) {
6814
- if (!currentMediaFieldId) {
7691
+ const { fieldId, hiddenInput, preview, actionsDiv } = getActiveMediaTarget();
7692
+ if (!fieldId || !hiddenInput) {
6815
7693
  console.error('No field ID set for media selection');
6816
7694
  return;
6817
7695
  }
6818
7696
 
6819
- const fieldId = currentMediaFieldId;
6820
-
6821
7697
  // 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
- }
7698
+ hiddenInput.value = mediaUrl;
7699
+ notifyFieldChange(hiddenInput);
6826
7700
 
6827
7701
  // Update the preview
6828
- const preview = document.getElementById(fieldId + '-preview');
6829
7702
  if (preview) {
6830
7703
  preview.innerHTML = \`<img src="\${mediaUrl}" alt="\${filename}" class="w-32 h-32 object-cover rounded-lg border border-white/20">\`;
6831
7704
  preview.classList.remove('hidden');
6832
7705
  }
6833
7706
 
6834
7707
  // 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
- }
7708
+ ensureSingleMediaRemoveButton(fieldId, actionsDiv);
6847
7709
 
6848
7710
  // DON'T close the modal - let user click OK button
6849
7711
  // Visual feedback: highlight the selected item
@@ -6857,7 +7719,9 @@ function renderContentFormPage(data) {
6857
7719
  };
6858
7720
 
6859
7721
  function setMediaField(fieldId, mediaUrl) {
6860
- document.getElementById(fieldId).value = mediaUrl;
7722
+ const hiddenInput = document.getElementById(fieldId);
7723
+ hiddenInput.value = mediaUrl;
7724
+ notifyFieldChange(hiddenInput);
6861
7725
  const preview = document.getElementById(fieldId + '-preview');
6862
7726
  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
7727
  preview.classList.remove('hidden');
@@ -8304,9 +9168,9 @@ function parseFieldValue(field, formData, options = {}) {
8304
9168
  const { skipValidation = false } = options;
8305
9169
  const value = formData.get(field.field_name);
8306
9170
  const errors = [];
8307
- const blocksConfig = chunkSHU7Q66Q_cjs.getBlocksFieldConfig(field.field_options);
9171
+ const blocksConfig = chunkEKPLKUZT_cjs.getBlocksFieldConfig(field.field_options);
8308
9172
  if (blocksConfig) {
8309
- const parsed = chunkSHU7Q66Q_cjs.parseBlocksValue(value, blocksConfig);
9173
+ const parsed = chunkEKPLKUZT_cjs.parseBlocksValue(value, blocksConfig);
8310
9174
  if (!skipValidation && field.is_required && parsed.value.length === 0) {
8311
9175
  parsed.errors.push(`${field.field_label} is required`);
8312
9176
  }
@@ -8416,7 +9280,7 @@ function extractFieldData(fields, formData, options = {}) {
8416
9280
  }
8417
9281
  return { data, errors };
8418
9282
  }
8419
- adminContentRoutes.use("*", chunkDQZVU3WB_cjs.requireAuth());
9283
+ adminContentRoutes.use("*", chunkIT2TC4ZD_cjs.requireAuth());
8420
9284
  async function getCollectionFields(db, collectionId) {
8421
9285
  const cache = chunk64APW3DW_cjs.getCacheService(chunk64APW3DW_cjs.CACHE_CONFIGS.collection);
8422
9286
  return cache.getOrSet(
@@ -8692,21 +9556,21 @@ adminContentRoutes.get("/new", async (c) => {
8692
9556
  const tinymceEnabled = await isPluginActive2(db, "tinymce-plugin");
8693
9557
  let tinymceSettings;
8694
9558
  if (tinymceEnabled) {
8695
- const pluginService = new chunkMPT5PA6U_cjs.PluginService(db);
9559
+ const pluginService = new chunkIIBRG5S5_cjs.PluginService(db);
8696
9560
  const tinymcePlugin2 = await pluginService.getPlugin("tinymce-plugin");
8697
9561
  tinymceSettings = tinymcePlugin2?.settings;
8698
9562
  }
8699
9563
  const quillEnabled = await isPluginActive2(db, "quill-editor");
8700
9564
  let quillSettings;
8701
9565
  if (quillEnabled) {
8702
- const pluginService = new chunkMPT5PA6U_cjs.PluginService(db);
9566
+ const pluginService = new chunkIIBRG5S5_cjs.PluginService(db);
8703
9567
  const quillPlugin = await pluginService.getPlugin("quill-editor");
8704
9568
  quillSettings = quillPlugin?.settings;
8705
9569
  }
8706
9570
  const mdxeditorEnabled = await isPluginActive2(db, "easy-mdx");
8707
9571
  let mdxeditorSettings;
8708
9572
  if (mdxeditorEnabled) {
8709
- const pluginService = new chunkMPT5PA6U_cjs.PluginService(db);
9573
+ const pluginService = new chunkIIBRG5S5_cjs.PluginService(db);
8710
9574
  const mdxeditorPlugin = await pluginService.getPlugin("easy-mdx");
8711
9575
  mdxeditorSettings = mdxeditorPlugin?.settings;
8712
9576
  }
@@ -8797,21 +9661,21 @@ adminContentRoutes.get("/:id/edit", async (c) => {
8797
9661
  const tinymceEnabled = await isPluginActive2(db, "tinymce-plugin");
8798
9662
  let tinymceSettings;
8799
9663
  if (tinymceEnabled) {
8800
- const pluginService = new chunkMPT5PA6U_cjs.PluginService(db);
9664
+ const pluginService = new chunkIIBRG5S5_cjs.PluginService(db);
8801
9665
  const tinymcePlugin2 = await pluginService.getPlugin("tinymce-plugin");
8802
9666
  tinymceSettings = tinymcePlugin2?.settings;
8803
9667
  }
8804
9668
  const quillEnabled = await isPluginActive2(db, "quill-editor");
8805
9669
  let quillSettings;
8806
9670
  if (quillEnabled) {
8807
- const pluginService = new chunkMPT5PA6U_cjs.PluginService(db);
9671
+ const pluginService = new chunkIIBRG5S5_cjs.PluginService(db);
8808
9672
  const quillPlugin = await pluginService.getPlugin("quill-editor");
8809
9673
  quillSettings = quillPlugin?.settings;
8810
9674
  }
8811
9675
  const mdxeditorEnabled = await isPluginActive2(db, "easy-mdx");
8812
9676
  let mdxeditorSettings;
8813
9677
  if (mdxeditorEnabled) {
8814
- const pluginService = new chunkMPT5PA6U_cjs.PluginService(db);
9678
+ const pluginService = new chunkIIBRG5S5_cjs.PluginService(db);
8815
9679
  const mdxeditorPlugin = await pluginService.getPlugin("easy-mdx");
8816
9680
  mdxeditorSettings = mdxeditorPlugin?.settings;
8817
9681
  }
@@ -9106,7 +9970,7 @@ adminContentRoutes.put("/:id", async (c) => {
9106
9970
  `);
9107
9971
  }
9108
9972
  });
9109
- adminContentRoutes.post("/preview", chunkDQZVU3WB_cjs.requireRole(["admin", "editor", "author"]), async (c) => {
9973
+ adminContentRoutes.post("/preview", chunkIT2TC4ZD_cjs.requireRole(["admin", "editor", "author"]), async (c) => {
9110
9974
  try {
9111
9975
  const formData = await c.req.formData();
9112
9976
  const collectionId = formData.get("collection_id");
@@ -9484,7 +10348,7 @@ adminContentRoutes.post("/:id/restore/:version", async (c) => {
9484
10348
  return c.json({ success: false, error: "Failed to restore version" });
9485
10349
  }
9486
10350
  });
9487
- adminContentRoutes.get("/:id/version/:version/preview", chunkDQZVU3WB_cjs.requireRole(["admin", "editor", "author"]), async (c) => {
10351
+ adminContentRoutes.get("/:id/version/:version/preview", chunkIT2TC4ZD_cjs.requireRole(["admin", "editor", "author"]), async (c) => {
9488
10352
  try {
9489
10353
  const id = c.req.param("id");
9490
10354
  const version = parseInt(c.req.param("version") || "0");
@@ -11451,7 +12315,14 @@ function renderUsersListPage(data) {
11451
12315
 
11452
12316
  // src/routes/admin-users.ts
11453
12317
  var userRoutes = new hono.Hono();
11454
- userRoutes.use("*", chunkDQZVU3WB_cjs.requireAuth());
12318
+ userRoutes.use("*", chunkIT2TC4ZD_cjs.requireAuth());
12319
+ userRoutes.use("/users/*", chunkIT2TC4ZD_cjs.requireRole(["admin"]));
12320
+ userRoutes.use("/users", chunkIT2TC4ZD_cjs.requireRole(["admin"]));
12321
+ userRoutes.use("/invite-user", chunkIT2TC4ZD_cjs.requireRole(["admin"]));
12322
+ userRoutes.use("/resend-invitation/*", chunkIT2TC4ZD_cjs.requireRole(["admin"]));
12323
+ userRoutes.use("/cancel-invitation/*", chunkIT2TC4ZD_cjs.requireRole(["admin"]));
12324
+ userRoutes.use("/activity-logs", chunkIT2TC4ZD_cjs.requireRole(["admin"]));
12325
+ userRoutes.use("/activity-logs/*", chunkIT2TC4ZD_cjs.requireRole(["admin"]));
11455
12326
  userRoutes.get("/", (c) => {
11456
12327
  return c.redirect("/admin/dashboard");
11457
12328
  });
@@ -11606,7 +12477,7 @@ userRoutes.put("/profile", async (c) => {
11606
12477
  Date.now(),
11607
12478
  user.userId
11608
12479
  ).run();
11609
- await chunkDQZVU3WB_cjs.logActivity(
12480
+ await chunkIT2TC4ZD_cjs.logActivity(
11610
12481
  db,
11611
12482
  user.userId,
11612
12483
  "profile.update",
@@ -11669,7 +12540,7 @@ userRoutes.post("/profile/avatar", async (c) => {
11669
12540
  SELECT first_name, last_name FROM users WHERE id = ?
11670
12541
  `);
11671
12542
  const userData = await userStmt.bind(user.userId).first();
11672
- await chunkDQZVU3WB_cjs.logActivity(
12543
+ await chunkIT2TC4ZD_cjs.logActivity(
11673
12544
  db,
11674
12545
  user.userId,
11675
12546
  "profile.avatar_update",
@@ -11740,7 +12611,7 @@ userRoutes.post("/profile/password", async (c) => {
11740
12611
  dismissible: true
11741
12612
  }));
11742
12613
  }
11743
- const validPassword = await chunkDQZVU3WB_cjs.AuthManager.verifyPassword(currentPassword, userData.password_hash);
12614
+ const validPassword = await chunkIT2TC4ZD_cjs.AuthManager.verifyPassword(currentPassword, userData.password_hash);
11744
12615
  if (!validPassword) {
11745
12616
  return c.html(renderAlert2({
11746
12617
  type: "error",
@@ -11748,7 +12619,7 @@ userRoutes.post("/profile/password", async (c) => {
11748
12619
  dismissible: true
11749
12620
  }));
11750
12621
  }
11751
- const newPasswordHash = await chunkDQZVU3WB_cjs.AuthManager.hashPassword(newPassword);
12622
+ const newPasswordHash = await chunkIT2TC4ZD_cjs.AuthManager.hashPassword(newPassword);
11752
12623
  const historyStmt = db.prepare(`
11753
12624
  INSERT INTO password_history (id, user_id, password_hash, created_at)
11754
12625
  VALUES (?, ?, ?, ?)
@@ -11764,7 +12635,7 @@ userRoutes.post("/profile/password", async (c) => {
11764
12635
  WHERE id = ?
11765
12636
  `);
11766
12637
  await updateStmt.bind(newPasswordHash, Date.now(), user.userId).run();
11767
- await chunkDQZVU3WB_cjs.logActivity(
12638
+ await chunkIT2TC4ZD_cjs.logActivity(
11768
12639
  db,
11769
12640
  user.userId,
11770
12641
  "profile.password_change",
@@ -11831,7 +12702,7 @@ userRoutes.get("/users", async (c) => {
11831
12702
  `);
11832
12703
  const countResult = await countStmt.bind(...params).first();
11833
12704
  const totalUsers = countResult?.total || 0;
11834
- await chunkDQZVU3WB_cjs.logActivity(
12705
+ await chunkIT2TC4ZD_cjs.logActivity(
11835
12706
  db,
11836
12707
  user.userId,
11837
12708
  "users.list_view",
@@ -11939,7 +12810,9 @@ userRoutes.post("/users/new", async (c) => {
11939
12810
  const email = formData.get("email")?.toString()?.trim().toLowerCase() || "";
11940
12811
  const phone = chunkMNWKYY5E_cjs.sanitizeInput(formData.get("phone")?.toString()) || null;
11941
12812
  const bio = chunkMNWKYY5E_cjs.sanitizeInput(formData.get("bio")?.toString()) || null;
11942
- const role = formData.get("role")?.toString() || "viewer";
12813
+ const roleInput = formData.get("role")?.toString() || "viewer";
12814
+ const validRoles = ["admin", "editor", "author", "viewer"];
12815
+ const role = validRoles.includes(roleInput) ? roleInput : "viewer";
11943
12816
  const password = formData.get("password")?.toString() || "";
11944
12817
  const confirmPassword = formData.get("confirm_password")?.toString() || "";
11945
12818
  const isActive = formData.get("is_active") === "1";
@@ -11985,7 +12858,7 @@ userRoutes.post("/users/new", async (c) => {
11985
12858
  dismissible: true
11986
12859
  }));
11987
12860
  }
11988
- const passwordHash = await chunkDQZVU3WB_cjs.AuthManager.hashPassword(password);
12861
+ const passwordHash = await chunkIT2TC4ZD_cjs.AuthManager.hashPassword(password);
11989
12862
  const userId = crypto.randomUUID();
11990
12863
  const createStmt = db.prepare(`
11991
12864
  INSERT INTO users (
@@ -12008,7 +12881,7 @@ userRoutes.post("/users/new", async (c) => {
12008
12881
  Date.now(),
12009
12882
  Date.now()
12010
12883
  ).run();
12011
- await chunkDQZVU3WB_cjs.logActivity(
12884
+ await chunkIT2TC4ZD_cjs.logActivity(
12012
12885
  db,
12013
12886
  user.userId,
12014
12887
  "user!.create",
@@ -12046,7 +12919,7 @@ userRoutes.get("/users/:id", async (c) => {
12046
12919
  if (!userRecord) {
12047
12920
  return c.json({ error: "User not found" }, 404);
12048
12921
  }
12049
- await chunkDQZVU3WB_cjs.logActivity(
12922
+ await chunkIT2TC4ZD_cjs.logActivity(
12050
12923
  db,
12051
12924
  user.userId,
12052
12925
  "user!.view",
@@ -12159,7 +13032,9 @@ userRoutes.put("/users/:id", async (c) => {
12159
13032
  const username = chunkMNWKYY5E_cjs.sanitizeInput(formData.get("username")?.toString());
12160
13033
  const email = formData.get("email")?.toString()?.trim().toLowerCase() || "";
12161
13034
  const phone = chunkMNWKYY5E_cjs.sanitizeInput(formData.get("phone")?.toString()) || null;
12162
- const role = formData.get("role")?.toString() || "viewer";
13035
+ const roleInput = formData.get("role")?.toString() || "viewer";
13036
+ const validRoles = ["admin", "editor", "author", "viewer"];
13037
+ const role = validRoles.includes(roleInput) ? roleInput : "viewer";
12163
13038
  const isActive = formData.get("is_active") === "1";
12164
13039
  const emailVerified = formData.get("email_verified") === "1";
12165
13040
  const profileDisplayName = chunkMNWKYY5E_cjs.sanitizeInput(formData.get("profile_display_name")?.toString()) || null;
@@ -12271,7 +13146,7 @@ userRoutes.put("/users/:id", async (c) => {
12271
13146
  ).run();
12272
13147
  }
12273
13148
  }
12274
- await chunkDQZVU3WB_cjs.logActivity(
13149
+ await chunkIT2TC4ZD_cjs.logActivity(
12275
13150
  db,
12276
13151
  user.userId,
12277
13152
  "user.update",
@@ -12316,7 +13191,7 @@ userRoutes.post("/users/:id/toggle", async (c) => {
12316
13191
  UPDATE users SET is_active = ?, updated_at = ? WHERE id = ?
12317
13192
  `);
12318
13193
  await toggleStmt.bind(active ? 1 : 0, Date.now(), userId).run();
12319
- await chunkDQZVU3WB_cjs.logActivity(
13194
+ await chunkIT2TC4ZD_cjs.logActivity(
12320
13195
  db,
12321
13196
  user.userId,
12322
13197
  active ? "user.activate" : "user.deactivate",
@@ -12357,7 +13232,7 @@ userRoutes.delete("/users/:id", async (c) => {
12357
13232
  DELETE FROM users WHERE id = ?
12358
13233
  `);
12359
13234
  await deleteStmt.bind(userId).run();
12360
- await chunkDQZVU3WB_cjs.logActivity(
13235
+ await chunkIT2TC4ZD_cjs.logActivity(
12361
13236
  db,
12362
13237
  user.userId,
12363
13238
  "user!.hard_delete",
@@ -12376,7 +13251,7 @@ userRoutes.delete("/users/:id", async (c) => {
12376
13251
  UPDATE users SET is_active = 0, updated_at = ? WHERE id = ?
12377
13252
  `);
12378
13253
  await deleteStmt.bind(Date.now(), userId).run();
12379
- await chunkDQZVU3WB_cjs.logActivity(
13254
+ await chunkIT2TC4ZD_cjs.logActivity(
12380
13255
  db,
12381
13256
  user.userId,
12382
13257
  "user!.soft_delete",
@@ -12442,7 +13317,7 @@ userRoutes.post("/invite-user", async (c) => {
12442
13317
  Date.now(),
12443
13318
  Date.now()
12444
13319
  ).run();
12445
- await chunkDQZVU3WB_cjs.logActivity(
13320
+ await chunkIT2TC4ZD_cjs.logActivity(
12446
13321
  db,
12447
13322
  user.userId,
12448
13323
  "user!.invite_sent",
@@ -12499,7 +13374,7 @@ userRoutes.post("/resend-invitation/:id", async (c) => {
12499
13374
  Date.now(),
12500
13375
  userId
12501
13376
  ).run();
12502
- await chunkDQZVU3WB_cjs.logActivity(
13377
+ await chunkIT2TC4ZD_cjs.logActivity(
12503
13378
  db,
12504
13379
  user.userId,
12505
13380
  "user!.invitation_resent",
@@ -12535,7 +13410,7 @@ userRoutes.delete("/cancel-invitation/:id", async (c) => {
12535
13410
  }
12536
13411
  const deleteStmt = db.prepare(`DELETE FROM users WHERE id = ?`);
12537
13412
  await deleteStmt.bind(userId).run();
12538
- await chunkDQZVU3WB_cjs.logActivity(
13413
+ await chunkIT2TC4ZD_cjs.logActivity(
12539
13414
  db,
12540
13415
  user.userId,
12541
13416
  "user!.invitation_cancelled",
@@ -12618,7 +13493,7 @@ userRoutes.get("/activity-logs", async (c) => {
12618
13493
  ...log,
12619
13494
  details: log.details ? JSON.parse(log.details) : null
12620
13495
  }));
12621
- await chunkDQZVU3WB_cjs.logActivity(
13496
+ await chunkIT2TC4ZD_cjs.logActivity(
12622
13497
  db,
12623
13498
  user.userId,
12624
13499
  "activity.logs_viewed",
@@ -12725,7 +13600,7 @@ userRoutes.get("/activity-logs/export", async (c) => {
12725
13600
  csvRows.push(row.join(","));
12726
13601
  }
12727
13602
  const csvContent = csvRows.join("\n");
12728
- await chunkDQZVU3WB_cjs.logActivity(
13603
+ await chunkIT2TC4ZD_cjs.logActivity(
12729
13604
  db,
12730
13605
  user.userId,
12731
13606
  "activity.logs_exported",
@@ -14064,7 +14939,7 @@ var fileValidationSchema2 = zod.z.object({
14064
14939
  // 50MB max
14065
14940
  });
14066
14941
  var adminMediaRoutes = new hono.Hono();
14067
- adminMediaRoutes.use("*", chunkDQZVU3WB_cjs.requireAuth());
14942
+ adminMediaRoutes.use("*", chunkIT2TC4ZD_cjs.requireAuth());
14068
14943
  adminMediaRoutes.get("/", async (c) => {
14069
14944
  try {
14070
14945
  const user = c.get("user");
@@ -14650,7 +15525,7 @@ adminMediaRoutes.put("/:id", async (c) => {
14650
15525
  `);
14651
15526
  }
14652
15527
  });
14653
- adminMediaRoutes.delete("/cleanup", chunkDQZVU3WB_cjs.requireRole("admin"), async (c) => {
15528
+ adminMediaRoutes.delete("/cleanup", chunkIT2TC4ZD_cjs.requireRole("admin"), async (c) => {
14654
15529
  try {
14655
15530
  const db = c.env.DB;
14656
15531
  const allMediaStmt = db.prepare("SELECT id, r2_key, filename FROM media WHERE deleted_at IS NULL");
@@ -16873,7 +17748,7 @@ function renderEmailSettingsContent(plugin, settings) {
16873
17748
 
16874
17749
  // src/routes/admin-plugins.ts
16875
17750
  var adminPluginRoutes = new hono.Hono();
16876
- adminPluginRoutes.use("*", chunkDQZVU3WB_cjs.requireAuth());
17751
+ adminPluginRoutes.use("*", chunkIT2TC4ZD_cjs.requireAuth());
16877
17752
  var AVAILABLE_PLUGINS = [
16878
17753
  {
16879
17754
  id: "third-party-faq",
@@ -17000,7 +17875,7 @@ adminPluginRoutes.get("/", async (c) => {
17000
17875
  if (user?.role !== "admin") {
17001
17876
  return c.text("Access denied", 403);
17002
17877
  }
17003
- const pluginService = new chunkMPT5PA6U_cjs.PluginService(db);
17878
+ const pluginService = new chunkIIBRG5S5_cjs.PluginService(db);
17004
17879
  let installedPlugins = [];
17005
17880
  let stats = { total: 0, active: 0, inactive: 0, errors: 0, uninstalled: 0 };
17006
17881
  try {
@@ -17076,7 +17951,7 @@ adminPluginRoutes.get("/:id", async (c) => {
17076
17951
  if (user?.role !== "admin") {
17077
17952
  return c.redirect("/admin/plugins");
17078
17953
  }
17079
- const pluginService = new chunkMPT5PA6U_cjs.PluginService(db);
17954
+ const pluginService = new chunkIIBRG5S5_cjs.PluginService(db);
17080
17955
  const plugin = await pluginService.getPlugin(pluginId);
17081
17956
  if (!plugin) {
17082
17957
  return c.text("Plugin not found", 404);
@@ -17160,7 +18035,7 @@ adminPluginRoutes.post("/:id/activate", async (c) => {
17160
18035
  if (user?.role !== "admin") {
17161
18036
  return c.json({ error: "Access denied" }, 403);
17162
18037
  }
17163
- const pluginService = new chunkMPT5PA6U_cjs.PluginService(db);
18038
+ const pluginService = new chunkIIBRG5S5_cjs.PluginService(db);
17164
18039
  await pluginService.activatePlugin(pluginId);
17165
18040
  return c.json({ success: true });
17166
18041
  } catch (error) {
@@ -17177,7 +18052,7 @@ adminPluginRoutes.post("/:id/deactivate", async (c) => {
17177
18052
  if (user?.role !== "admin") {
17178
18053
  return c.json({ error: "Access denied" }, 403);
17179
18054
  }
17180
- const pluginService = new chunkMPT5PA6U_cjs.PluginService(db);
18055
+ const pluginService = new chunkIIBRG5S5_cjs.PluginService(db);
17181
18056
  await pluginService.deactivatePlugin(pluginId);
17182
18057
  return c.json({ success: true });
17183
18058
  } catch (error) {
@@ -17194,7 +18069,7 @@ adminPluginRoutes.post("/install", async (c) => {
17194
18069
  return c.json({ error: "Access denied" }, 403);
17195
18070
  }
17196
18071
  const body = await c.req.json();
17197
- const pluginService = new chunkMPT5PA6U_cjs.PluginService(db);
18072
+ const pluginService = new chunkIIBRG5S5_cjs.PluginService(db);
17198
18073
  if (body.name === "faq-plugin") {
17199
18074
  const faqPlugin = await pluginService.installPlugin({
17200
18075
  id: "third-party-faq",
@@ -17464,7 +18339,7 @@ adminPluginRoutes.post("/:id/uninstall", async (c) => {
17464
18339
  if (user?.role !== "admin") {
17465
18340
  return c.json({ error: "Access denied" }, 403);
17466
18341
  }
17467
- const pluginService = new chunkMPT5PA6U_cjs.PluginService(db);
18342
+ const pluginService = new chunkIIBRG5S5_cjs.PluginService(db);
17468
18343
  await pluginService.uninstallPlugin(pluginId);
17469
18344
  return c.json({ success: true });
17470
18345
  } catch (error) {
@@ -17482,8 +18357,20 @@ adminPluginRoutes.post("/:id/settings", async (c) => {
17482
18357
  return c.json({ error: "Access denied" }, 403);
17483
18358
  }
17484
18359
  const settings = await c.req.json();
17485
- const pluginService = new chunkMPT5PA6U_cjs.PluginService(db);
18360
+ const pluginService = new chunkIIBRG5S5_cjs.PluginService(db);
17486
18361
  await pluginService.updatePluginSettings(pluginId, settings);
18362
+ if (pluginId === "core-auth") {
18363
+ try {
18364
+ const cacheKv = c.env.CACHE_KV;
18365
+ if (cacheKv) {
18366
+ await cacheKv.delete("auth:settings");
18367
+ await cacheKv.delete("auth:registration-enabled");
18368
+ console.log("[AuthSettings] Cache cleared after updating authentication settings");
18369
+ }
18370
+ } catch (cacheError) {
18371
+ console.error("[AuthSettings] Failed to clear cache:", cacheError);
18372
+ }
18373
+ }
17487
18374
  return c.json({ success: true });
17488
18375
  } catch (error) {
17489
18376
  console.error("Error updating plugin settings:", error);
@@ -18278,7 +19165,7 @@ function renderLogConfigPage(data) {
18278
19165
 
18279
19166
  // src/routes/admin-logs.ts
18280
19167
  var adminLogsRoutes = new hono.Hono();
18281
- adminLogsRoutes.use("*", chunkDQZVU3WB_cjs.requireAuth());
19168
+ adminLogsRoutes.use("*", chunkIT2TC4ZD_cjs.requireAuth());
18282
19169
  adminLogsRoutes.get("/", async (c) => {
18283
19170
  try {
18284
19171
  const user = c.get("user");
@@ -20606,9 +21493,9 @@ function renderStorageUsage(databaseSizeBytes, mediaSizeBytes) {
20606
21493
  }
20607
21494
 
20608
21495
  // src/routes/admin-dashboard.ts
20609
- var VERSION = chunkSHU7Q66Q_cjs.getCoreVersion();
21496
+ var VERSION = chunkEKPLKUZT_cjs.getCoreVersion();
20610
21497
  var router = new hono.Hono();
20611
- router.use("*", chunkDQZVU3WB_cjs.requireAuth());
21498
+ router.use("*", chunkIT2TC4ZD_cjs.requireAuth());
20612
21499
  router.get("/", async (c) => {
20613
21500
  const user = c.get("user");
20614
21501
  try {
@@ -22427,7 +23314,10 @@ function renderCollectionFormPage(data) {
22427
23314
 
22428
23315
  // src/routes/admin-collections.ts
22429
23316
  var adminCollectionsRoutes = new hono.Hono();
22430
- adminCollectionsRoutes.use("*", chunkDQZVU3WB_cjs.requireAuth());
23317
+ adminCollectionsRoutes.use("*", chunkIT2TC4ZD_cjs.requireAuth());
23318
+ adminCollectionsRoutes.post("*", chunkIT2TC4ZD_cjs.requireRole(["admin"]));
23319
+ adminCollectionsRoutes.put("*", chunkIT2TC4ZD_cjs.requireRole(["admin"]));
23320
+ adminCollectionsRoutes.delete("*", chunkIT2TC4ZD_cjs.requireRole(["admin"]));
22431
23321
  adminCollectionsRoutes.get("/", async (c) => {
22432
23322
  try {
22433
23323
  const user = c.get("user");
@@ -24622,7 +25512,7 @@ function renderDatabaseToolsSettings(settings) {
24622
25512
 
24623
25513
  // src/routes/admin-settings.ts
24624
25514
  var adminSettingsRoutes = new hono.Hono();
24625
- adminSettingsRoutes.use("*", chunkDQZVU3WB_cjs.requireAuth());
25515
+ adminSettingsRoutes.use("*", chunkIT2TC4ZD_cjs.requireAuth());
24626
25516
  function getMockSettings(user) {
24627
25517
  return {
24628
25518
  general: {
@@ -24790,7 +25680,7 @@ adminSettingsRoutes.get("/database-tools", (c) => {
24790
25680
  adminSettingsRoutes.get("/api/migrations/status", async (c) => {
24791
25681
  try {
24792
25682
  const db = c.env.DB;
24793
- const migrationService = new chunkLDFMYRG6_cjs.MigrationService(db);
25683
+ const migrationService = new chunkZMVWMJ3S_cjs.MigrationService(db);
24794
25684
  const status = await migrationService.getMigrationStatus();
24795
25685
  return c.json({
24796
25686
  success: true,
@@ -24814,7 +25704,7 @@ adminSettingsRoutes.post("/api/migrations/run", async (c) => {
24814
25704
  }, 403);
24815
25705
  }
24816
25706
  const db = c.env.DB;
24817
- const migrationService = new chunkLDFMYRG6_cjs.MigrationService(db);
25707
+ const migrationService = new chunkZMVWMJ3S_cjs.MigrationService(db);
24818
25708
  const result = await migrationService.runPendingMigrations();
24819
25709
  return c.json({
24820
25710
  success: result.success,
@@ -24832,7 +25722,7 @@ adminSettingsRoutes.post("/api/migrations/run", async (c) => {
24832
25722
  adminSettingsRoutes.get("/api/migrations/validate", async (c) => {
24833
25723
  try {
24834
25724
  const db = c.env.DB;
24835
- const migrationService = new chunkLDFMYRG6_cjs.MigrationService(db);
25725
+ const migrationService = new chunkZMVWMJ3S_cjs.MigrationService(db);
24836
25726
  const validation = await migrationService.validateSchema();
24837
25727
  return c.json({
24838
25728
  success: true,
@@ -26722,7 +27612,7 @@ function renderFormCreatePage(data) {
26722
27612
 
26723
27613
  // src/routes/admin-forms.ts
26724
27614
  var adminFormsRoutes = new hono.Hono();
26725
- adminFormsRoutes.use("*", chunkDQZVU3WB_cjs.requireAuth());
27615
+ adminFormsRoutes.use("*", chunkIT2TC4ZD_cjs.requireAuth());
26726
27616
  adminFormsRoutes.get("/", async (c) => {
26727
27617
  try {
26728
27618
  const user = c.get("user");
@@ -27873,9 +28763,9 @@ function renderAPIReferencePage(data) {
27873
28763
  }
27874
28764
 
27875
28765
  // src/routes/admin-api-reference.ts
27876
- var VERSION2 = chunkSHU7Q66Q_cjs.getCoreVersion();
28766
+ var VERSION2 = chunkEKPLKUZT_cjs.getCoreVersion();
27877
28767
  var router2 = new hono.Hono();
27878
- router2.use("*", chunkDQZVU3WB_cjs.requireAuth());
28768
+ router2.use("*", chunkIT2TC4ZD_cjs.requireAuth());
27879
28769
  router2.get("/", async (c) => {
27880
28770
  const user = c.get("user");
27881
28771
  try {
@@ -27962,5 +28852,5 @@ exports.router = router;
27962
28852
  exports.router2 = router2;
27963
28853
  exports.test_cleanup_default = test_cleanup_default;
27964
28854
  exports.userRoutes = userRoutes;
27965
- //# sourceMappingURL=chunk-KSB6FXOP.cjs.map
27966
- //# sourceMappingURL=chunk-KSB6FXOP.cjs.map
28855
+ //# sourceMappingURL=chunk-RCA6R6VE.cjs.map
28856
+ //# sourceMappingURL=chunk-RCA6R6VE.cjs.map