memi-board 0.51.2 → 0.52.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 (43) hide show
  1. package/dist/{ar-Dzc31QaB.js → ar-BVwpedHS.js} +23 -14
  2. package/dist/ar-BVwpedHS.js.map +1 -0
  3. package/dist/{commentImages-CV0g3ARh.js → commentImages-CzXO42gE.js} +39 -23
  4. package/dist/commentImages-CzXO42gE.js.map +1 -0
  5. package/dist/composables/useMemiBoardWritingAssistant.d.ts +2 -0
  6. package/dist/data/versionHistory.d.ts +5 -0
  7. package/dist/{de-Wci_gqEw.js → de-K2gYwR8U.js} +23 -14
  8. package/dist/de-K2gYwR8U.js.map +1 -0
  9. package/dist/{en-Dhe-tFzi.js → en-CmPLbFcN.js} +30 -14
  10. package/dist/en-CmPLbFcN.js.map +1 -0
  11. package/dist/{es-BHJ1VeX-.js → es-1P1miLJH.js} +23 -14
  12. package/dist/es-1P1miLJH.js.map +1 -0
  13. package/dist/{fr-aSscFSwK.js → fr-cDQCmNSc.js} +23 -14
  14. package/dist/fr-cDQCmNSc.js.map +1 -0
  15. package/dist/{id-BuUIYpdS.js → id-BzDIqeZ4.js} +23 -14
  16. package/dist/id-BzDIqeZ4.js.map +1 -0
  17. package/dist/index.d.ts +1 -1
  18. package/dist/index.js +597 -427
  19. package/dist/index.js.map +1 -1
  20. package/dist/{ja-owJ_AiPl.js → ja-B2wxHGCr.js} +23 -14
  21. package/dist/ja-B2wxHGCr.js.map +1 -0
  22. package/dist/{pt-B3Uxrkz2.js → pt-BCY3OODD.js} +23 -14
  23. package/dist/pt-BCY3OODD.js.map +1 -0
  24. package/dist/runtime/components/CategorySettings.vue +37 -9
  25. package/dist/runtime/components/Editor.vue +9 -4
  26. package/dist/runtime/components/Settings.vue +198 -9
  27. package/dist/runtime/components/VersionHistory.vue +9 -2
  28. package/dist/storage.js +1 -1
  29. package/dist/types.d.ts +5 -0
  30. package/dist/utils/localizedFields.d.ts +12 -2
  31. package/dist/{zh-B49hHRSL.js → zh-Y824rwR6.js} +23 -14
  32. package/dist/zh-Y824rwR6.js.map +1 -0
  33. package/package.json +1 -1
  34. package/dist/ar-Dzc31QaB.js.map +0 -1
  35. package/dist/commentImages-CV0g3ARh.js.map +0 -1
  36. package/dist/de-Wci_gqEw.js.map +0 -1
  37. package/dist/en-Dhe-tFzi.js.map +0 -1
  38. package/dist/es-BHJ1VeX-.js.map +0 -1
  39. package/dist/fr-aSscFSwK.js.map +0 -1
  40. package/dist/id-BuUIYpdS.js.map +0 -1
  41. package/dist/ja-owJ_AiPl.js.map +0 -1
  42. package/dist/pt-B3Uxrkz2.js.map +0 -1
  43. package/dist/zh-B49hHRSL.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { A as e, B as t, C as n, D as r, E as i, F as a, G as o, H as s, I as c, J as l, K as u, L as d, M as f, N as p, O as m, P as h, R as g, S as _, T as v, U as y, V as b, W as x, X as S, Y as C, Z as w, _ as T, a as E, b as D, c as ee, d as O, f as te, g as k, h as ne, j as re, k as ie, l as ae, m as A, o as oe, p as se, q as ce, r as le, s as j, t as ue, u as de, v as fe, w as M, x as pe, y as N, z as me } from "./commentImages-CV0g3ARh.js";
1
+ import { A as e, B as t, C as n, D as r, E as i, F as a, G as o, H as s, I as c, J as l, K as u, L as d, M as f, N as p, O as m, P as h, R as g, S as _, T as v, U as y, V as b, W as x, X as S, Y as C, Z as w, _ as T, a as E, b as D, c as ee, d as O, f as te, g as k, h as ne, j as re, k as ie, l as ae, m as A, o as oe, p as se, q as ce, r as le, s as j, t as ue, u as de, v as fe, w as M, x as pe, y as N, z as me } from "./commentImages-CzXO42gE.js";
2
2
  import { computed as P, onScopeDispose as F, reactive as he, readonly as ge, ref as I, toValue as L, watch as R } from "vue";
3
3
  import { collection as _e, deleteDoc as ve, deleteField as ye, doc as z, documentId as B, endAt as be, endBefore as xe, getDoc as V, getDocFromServer as Se, getDocs as H, getDocsFromServer as Ce, increment as we, limit as U, limitToLast as Te, onSnapshot as W, orderBy as G, query as K, runTransaction as Ee, serverTimestamp as q, setDoc as De, startAfter as J, updateDoc as Y, where as X, writeBatch as Oe } from "firebase/firestore";
4
4
  import { useCollection as ke, useCurrentUser as Ae, useDocument as je, useFirebaseApp as Me, useFirebaseAuth as Ne, useFirestore as Z } from "vuefire";
@@ -121,10 +121,19 @@ function ft(e, t, n) {
121
121
  ...i
122
122
  }), r;
123
123
  }
124
- function pt(e, t) {
125
- return !e || !t.length ? !1 : T.every((n) => t.every((t) => !!lt(e[n.code], t)));
124
+ function pt(e) {
125
+ let t = new Set((e?.translationLocales ?? []).map((e) => N(e)).filter((e) => !!e)), n = T.map((e) => e.code);
126
+ return t.size ? n.filter((e) => t.has(e)) : n;
126
127
  }
127
- function mt(e, t, r, i, a) {
128
+ function mt(e) {
129
+ if (!Array.isArray(e)) return [];
130
+ let t = pt({ translationLocales: e.filter((e) => typeof e == "string") });
131
+ return t.length === T.length ? [] : t;
132
+ }
133
+ function ht(e, t, n = T.map((e) => e.code)) {
134
+ return !e || !t.length ? !1 : n.every((n) => t.every((t) => !!lt(e[n], t)));
135
+ }
136
+ function gt(e, t, r, i, a) {
128
137
  if (!n()) return i;
129
138
  let o = N(t), s = N(a);
130
139
  if (!o || s && o === s) return i;
@@ -136,32 +145,32 @@ function mt(e, t, r, i, a) {
136
145
  }
137
146
  return i;
138
147
  }
139
- function ht(e, t, n, r, i) {
140
- return r ? !t || t[n] !== r ? r : mt(e, i, n, r, t.locale) : "";
148
+ function _t(e, t, n, r, i) {
149
+ return r ? !t || t[n] !== r ? r : gt(e, i, n, r, t.locale) : "";
141
150
  }
142
- function gt(e, t) {
143
- return ht(e.i18n, e.i18nSource, "title", e.title ?? "", t);
151
+ function vt(e, t) {
152
+ return _t(e.i18n, e.i18nSource, "title", e.title ?? "", t);
144
153
  }
145
- function _t(e, t) {
146
- return ht(e.i18n, e.i18nSource, "summary", e.summary ?? "", t);
154
+ function yt(e, t) {
155
+ return _t(e.i18n, e.i18nSource, "summary", e.summary ?? "", t);
147
156
  }
148
- function vt(e, t) {
157
+ function bt(e, t) {
149
158
  let n = e.content || "", r = e.contentI18nSource?.hash;
150
- return !n || !r || r !== ct(n) ? n : mt(e.contentI18n, t, "content", n, e.contentI18nSource?.locale) || n;
159
+ return !n || !r || r !== ct(n) ? n : gt(e.contentI18n, t, "content", n, e.contentI18nSource?.locale) || n;
151
160
  }
152
- function yt(e) {
161
+ function xt(e) {
153
162
  let t = e.label.trim(), n = (e.description ?? "").trim();
154
- return !t || !e.i18nSource || e.i18nSource.label !== t || (e.i18nSource.description ?? "") !== n ? !1 : pt(e.i18n, n ? ["label", "description"] : ["label"]);
163
+ return !t || !e.i18nSource || e.i18nSource.label !== t || (e.i18nSource.description ?? "") !== n ? !1 : ht(e.i18n, n ? ["label", "description"] : ["label"], pt(e));
155
164
  }
156
165
  //#endregion
157
166
  //#region src/composables/fetchPublicSeo.ts
158
- function bt() {
167
+ function St() {
159
168
  return {
160
169
  db: Z(),
161
170
  paths: M()
162
171
  };
163
172
  }
164
- async function xt(e, t, n) {
173
+ async function Ct(e, t, n) {
165
174
  try {
166
175
  let r = await V(w(e.db, e.paths, t));
167
176
  if (!r.exists()) return {
@@ -171,8 +180,8 @@ async function xt(e, t, n) {
171
180
  };
172
181
  let i = r.data() ?? {}, a = typeof i.label == "string" && i.label.trim() ? i.label.trim() : t, o = typeof i.description == "string" ? i.description.trim() : "", s = Q(i.i18n), c = ut(i.i18nSource);
173
182
  return {
174
- label: ht(s, c, "label", a, n) || a,
175
- description: ht(s, c, "description", o, n),
183
+ label: _t(s, c, "label", a, n) || a,
184
+ description: _t(s, c, "description", o, n),
176
185
  hidden: i.visibility === "hidden"
177
186
  };
178
187
  } catch {
@@ -183,7 +192,7 @@ async function xt(e, t, n) {
183
192
  };
184
193
  }
185
194
  }
186
- function St(e) {
195
+ function wt(e) {
187
196
  if (e && typeof e == "object" && "toDate" in e) try {
188
197
  let t = e.toDate();
189
198
  return t instanceof Date ? t.toISOString() : null;
@@ -192,17 +201,17 @@ function St(e) {
192
201
  }
193
202
  return null;
194
203
  }
195
- async function Ct(e, t, n, r) {
204
+ async function Tt(e, t, n, r) {
196
205
  let i = e.trim(), a = t.trim();
197
206
  if (!i || !a) return null;
198
- let s = n ?? bt();
207
+ let s = n ?? St();
199
208
  s.paths = C(s.paths), r && await se(r);
200
- let c = await xt(s, i, r);
209
+ let c = await Ct(s, i, r);
201
210
  if (c.hidden) return null;
202
211
  try {
203
212
  let e = (await H(K(o(s.db, s.paths), X("boardId", "==", i), X("slug", "==", a), X("listed", "==", !0), X("isPublished", "==", !0), U(1)))).docs[0];
204
213
  if (!e) return null;
205
- let t = e.data() ?? {}, n = typeof t.title == "string" ? t.title : "", l = typeof t.summary == "string" ? t.summary : "", u = Q(t.i18n), d = ut(t.i18nSource), f = gt({
214
+ let t = e.data() ?? {}, n = typeof t.title == "string" ? t.title : "", l = typeof t.summary == "string" ? t.summary : "", u = Q(t.i18n), d = ut(t.i18nSource), f = vt({
206
215
  title: n,
207
216
  i18n: u,
208
217
  i18nSource: d
@@ -213,24 +222,24 @@ async function Ct(e, t, n, r) {
213
222
  category: i,
214
223
  categoryLabel: c.label,
215
224
  title: f,
216
- summary: _t({
225
+ summary: yt({
217
226
  summary: l,
218
227
  i18n: u,
219
228
  i18nSource: d
220
229
  }, r).trim().slice(0, 200),
221
230
  previewImage: et(t.previewImage),
222
231
  authorName: typeof t.authorName == "string" && t.authorName.trim() ? t.authorName.trim() : null,
223
- createdAt: St(t.createdAt)
232
+ createdAt: wt(t.createdAt)
224
233
  } : null;
225
234
  } catch (e) {
226
235
  return import.meta.dev && console.warn("[memi-board] fetchPublicPostForSeo failed", e), null;
227
236
  }
228
237
  }
229
- async function wt(e, t, n) {
238
+ async function Et(e, t, n) {
230
239
  n && await se(n);
231
- let r = e?.trim() || null, i = t ?? bt();
240
+ let r = e?.trim() || null, i = t ?? St();
232
241
  if (i.paths = C(i.paths), r) {
233
- let e = await xt(i, r, n);
242
+ let e = await Ct(i, r, n);
234
243
  if (e.hidden) return {
235
244
  kind: "category",
236
245
  category: r,
@@ -239,7 +248,7 @@ async function wt(e, t, n) {
239
248
  ogImage: null,
240
249
  recentTitles: []
241
250
  };
242
- let { recentTitles: t, ogImage: a } = await Tt(i, r, n);
251
+ let { recentTitles: t, ogImage: a } = await Dt(i, r, n);
243
252
  return {
244
253
  kind: "category",
245
254
  category: r,
@@ -258,12 +267,12 @@ async function wt(e, t, n) {
258
267
  recentTitles: []
259
268
  };
260
269
  }
261
- async function Tt(e, t, n) {
270
+ async function Dt(e, t, n) {
262
271
  let r = [], i = null;
263
272
  try {
264
273
  let a = await H(K(o(e.db, e.paths), X("boardId", "==", t), X("listed", "==", !0), X("isPublished", "==", !0), G("createdAt", "desc"), U(12)));
265
274
  for (let e of a.docs) {
266
- let t = e.data() ?? {}, a = gt({
275
+ let t = e.data() ?? {}, a = vt({
267
276
  title: typeof t.title == "string" ? t.title : "",
268
277
  i18n: Q(t.i18n),
269
278
  i18nSource: ut(t.i18nSource)
@@ -280,31 +289,31 @@ async function Tt(e, t, n) {
280
289
  }
281
290
  //#endregion
282
291
  //#region src/utils/boardAccess.ts
283
- function Et(e, t) {
292
+ function Ot(e, t) {
284
293
  return !e || !t ? !1 : [...t.allowedStaffUids ?? [], ...t.allowedManagerUids ?? []].includes(e);
285
294
  }
286
- function Dt(e, t, n, r) {
287
- return e ? t ? !0 : n ? Et(e, r) : !1 : !1;
295
+ function kt(e, t, n, r) {
296
+ return e ? t ? !0 : n ? Ot(e, r) : !1 : !1;
288
297
  }
289
- function Ot(e, t, n, r) {
298
+ function At(e, t, n, r) {
290
299
  if (!e) return !1;
291
- if (r?.visibility === "hidden") return Dt(e, t, n, r);
300
+ if (r?.visibility === "hidden") return kt(e, t, n, r);
292
301
  let i = r?.commentWriteRole ?? "user";
293
- return i === "admin" ? t : i !== "staff" || Dt(e, t, n, r);
302
+ return i === "admin" ? t : i !== "staff" || kt(e, t, n, r);
294
303
  }
295
304
  //#endregion
296
305
  //#region src/composables/boardLookup.ts
297
- var kt;
298
- function At(e) {
299
- kt = e;
306
+ var jt;
307
+ function Mt(e) {
308
+ jt = e;
300
309
  }
301
- function jt(e) {
302
- return kt?.(e);
310
+ function Nt(e) {
311
+ return jt?.(e);
303
312
  }
304
313
  //#endregion
305
314
  //#region src/utils/moderation-strike.ts
306
- var Mt = 10, Nt = 864e5;
307
- function Pt(e, t, n = Date.now(), r = Nt) {
315
+ var Pt = 10, Ft = 864e5;
316
+ function It(e, t, n = Date.now(), r = Ft) {
308
317
  let i = Math.max(0, Math.floor(Number(e) || 0));
309
318
  if (i <= 0) return 0;
310
319
  let a = typeof t == "number" && Number.isFinite(t) ? t : null;
@@ -312,7 +321,7 @@ function Pt(e, t, n = Date.now(), r = Nt) {
312
321
  let o = Math.floor((n - a) / r);
313
322
  return Math.max(0, i - o);
314
323
  }
315
- function Ft(e, t, n = Date.now(), r = 10, i = Nt) {
324
+ function Lt(e, t, n = Date.now(), r = 10, i = Ft) {
316
325
  let a = Math.max(0, Math.floor(Number(e) || 0));
317
326
  if (a < r) return null;
318
327
  let o = typeof t == "number" && Number.isFinite(t) ? t : null;
@@ -320,10 +329,10 @@ function Ft(e, t, n = Date.now(), r = 10, i = Nt) {
320
329
  let s = o + (a - (r - 1)) * i;
321
330
  return s > n ? s : null;
322
331
  }
323
- function It(e, t, n = Date.now(), r = 10, i = Nt) {
324
- return Ft(e, t, n, r, i) != null;
332
+ function Rt(e, t, n = Date.now(), r = 10, i = Ft) {
333
+ return Lt(e, t, n, r, i) != null;
325
334
  }
326
- function Lt(e, t = Date.now(), n) {
335
+ function zt(e, t = Date.now(), n) {
327
336
  let r = Math.max(0, e - t), i = Math.ceil(r / 36e5);
328
337
  if (i <= 0) return A(n, "moderationStrike.liftSoon");
329
338
  if (i < 24) return A(n, "moderationStrike.liftInHours", { hours: i });
@@ -333,7 +342,7 @@ function Lt(e, t = Date.now(), n) {
333
342
  hours: o
334
343
  });
335
344
  }
336
- function Rt(e) {
345
+ function Bt(e) {
337
346
  if (e == null) return null;
338
347
  if (typeof e == "number" && Number.isFinite(e)) return e;
339
348
  if (typeof e == "object" && e && "toMillis" in e && typeof e.toMillis == "function") return e.toMillis();
@@ -345,26 +354,26 @@ function Rt(e) {
345
354
  }
346
355
  //#endregion
347
356
  //#region src/composables/useMemiBoardAuth.ts
348
- var zt = "__MEMI_BOARD_AUTH_STATE__";
349
- function Bt() {
357
+ var Vt = "__MEMI_BOARD_AUTH_STATE__";
358
+ function Ht() {
350
359
  let e = globalThis;
351
- return e[zt] || (e[zt] = {
360
+ return e[Vt] || (e[Vt] = {
352
361
  role: I("user"),
353
362
  rolePending: I(!0),
354
363
  rawBlockCount: I(0),
355
364
  blockAtMs: I(null),
356
365
  watchStarted: !1
357
- }), e[zt];
366
+ }), e[Vt];
358
367
  }
359
- function Vt() {
360
- let e = v(), { t, locale: n } = j(), r = Bt(), i = Z(), o = Ne(), s = Ae(), { role: c, rolePending: l, rawBlockCount: u, blockAtMs: d } = r;
368
+ function Ut() {
369
+ let e = v(), { t, locale: n } = j(), r = Ht(), i = Z(), o = Ne(), s = Ae(), { role: c, rolePending: l, rawBlockCount: u, blockAtMs: d } = r;
361
370
  function f() {
362
371
  let t = e.moderation?.blockBanThreshold;
363
372
  return t === 0 ? 0 : typeof t == "number" && t > 0 ? Math.floor(t) : 10;
364
373
  }
365
374
  function p() {
366
375
  let t = e.moderation?.blockBanDecayMs;
367
- return typeof t == "number" && t > 0 ? t : Nt;
376
+ return typeof t == "number" && t > 0 ? t : Ft;
368
377
  }
369
378
  function m(e) {
370
379
  return h(i, M(), e);
@@ -380,7 +389,7 @@ function Vt() {
380
389
  u.value = 0, d.value = null;
381
390
  return;
382
391
  }
383
- u.value = Math.max(0, Math.floor(Number(e.moderationBlockCount) || 0)), d.value = Rt(e.moderationBlockAt);
392
+ u.value = Math.max(0, Math.floor(Number(e.moderationBlockCount) || 0)), d.value = Bt(e.moderationBlockAt);
384
393
  }
385
394
  async function _(e) {
386
395
  let t = m(e.uid), n = a(i, M(), e.uid), r = await V(t);
@@ -428,14 +437,14 @@ function Vt() {
428
437
  l.value = !1;
429
438
  }
430
439
  }, { immediate: !0 }));
431
- let b = P(() => !!s.value), x = P(() => c.value === "owner"), S = P(() => c.value === "manager"), C = P(() => x.value || c.value === "admin"), w = P(() => S.value || c.value === "staff"), T = P(() => C.value || w.value), E = P(() => Pt(u.value, d.value, Date.now(), p())), D = P(() => {
440
+ let b = P(() => !!s.value), x = P(() => c.value === "owner"), S = P(() => c.value === "manager"), C = P(() => x.value || c.value === "admin"), w = P(() => S.value || c.value === "staff"), T = P(() => C.value || w.value), E = P(() => It(u.value, d.value, Date.now(), p())), D = P(() => {
432
441
  let e = f();
433
- return e <= 0 ? !1 : It(u.value, d.value, Date.now(), e, p());
442
+ return e <= 0 ? !1 : Rt(u.value, d.value, Date.now(), e, p());
434
443
  }), ee = P(() => {
435
444
  let e = f();
436
445
  if (e <= 0) return null;
437
- let t = Ft(u.value, d.value, Date.now(), e, p());
438
- return t == null ? null : Lt(t, Date.now(), n.value);
446
+ let t = Lt(u.value, d.value, Date.now(), e, p());
447
+ return t == null ? null : zt(t, Date.now(), n.value);
439
448
  }), O = P(() => {
440
449
  if (!D.value) return null;
441
450
  let e = ee.value;
@@ -462,12 +471,12 @@ function Vt() {
462
471
  });
463
472
  let o = (a.exists() ? Math.max(0, Math.floor(Number(a.data().moderationBlockCount) || 0)) : 0) + 1, c = Date.now();
464
473
  u.value = o, d.value = c;
465
- let l = Pt(o, c, c, p()), h = It(o, c, c, r, p()), g = t("auth.strikeSuffix", {
474
+ let l = It(o, c, c, p()), h = Rt(o, c, c, r, p()), g = t("auth.strikeSuffix", {
466
475
  count: l,
467
476
  threshold: r
468
477
  });
469
478
  if (h) {
470
- let e = Ft(o, c, c, r, p()), i = e == null ? null : Lt(e, c, n.value);
479
+ let e = Lt(o, c, c, r, p()), i = e == null ? null : zt(e, c, n.value);
471
480
  g = i ? t("auth.strikeRestrictedUntil", {
472
481
  count: l,
473
482
  until: i
@@ -501,10 +510,10 @@ function Vt() {
501
510
  await Be(k());
502
511
  }
503
512
  function oe(e) {
504
- return Dt(s.value?.uid, C.value, w.value, e ? jt(e) : void 0);
513
+ return kt(s.value?.uid, C.value, w.value, e ? Nt(e) : void 0);
505
514
  }
506
515
  function se(e) {
507
- return s.value ? C.value ? !0 : S.value && !!e && (jt(e)?.allowedManagerUids ?? []).includes(s.value.uid) : !1;
516
+ return s.value ? C.value ? !0 : S.value && !!e && (Nt(e)?.allowedManagerUids ?? []).includes(s.value.uid) : !1;
508
517
  }
509
518
  function ce(e) {
510
519
  return b.value && (s.value?.uid === e.authorUid || oe(e.boardId));
@@ -546,34 +555,34 @@ function Vt() {
546
555
  }
547
556
  //#endregion
548
557
  //#region src/utils/imeSafeSubmit.ts
549
- var Ht = /* @__PURE__ */ new WeakMap();
550
- function Ut(e) {
558
+ var Wt = /* @__PURE__ */ new WeakMap();
559
+ function Gt(e) {
551
560
  let t = e.currentTarget;
552
561
  return t instanceof HTMLButtonElement ? t : t instanceof Element ? t.closest("button") : null;
553
562
  }
554
- function Wt(e) {
563
+ function Kt(e) {
555
564
  if (e.pointerType !== "mouse" || e.button !== 0) return;
556
- let t = Ut(e), n = t?.form;
565
+ let t = Gt(e), n = t?.form;
557
566
  if (!t || !n || t.disabled) return;
558
- Ht.set(t, Date.now() + 1e3);
567
+ Wt.set(t, Date.now() + 1e3);
559
568
  let r = document.activeElement;
560
569
  r instanceof HTMLElement && r.blur(), setTimeout(() => n.requestSubmit(t), 0);
561
570
  }
562
- function Gt(e) {
563
- let t = Ut(e);
571
+ function qt(e) {
572
+ let t = Gt(e);
564
573
  if (!t) return;
565
- let n = Ht.get(t);
566
- n && Date.now() < n && (e.preventDefault(), Ht.delete(t));
574
+ let n = Wt.get(t);
575
+ n && Date.now() < n && (e.preventDefault(), Wt.delete(t));
567
576
  }
568
577
  //#endregion
569
578
  //#region src/utils/findReadablePost.ts
570
- async function Kt(e, t, n, r, i, a) {
579
+ async function Jt(e, t, n, r, i, a) {
571
580
  let s = [X("boardId", "==", n), X("slug", "==", r)];
572
581
  return a ? (await H(K(o(e, t), ...s, U(1)))).docs[0] : (await Promise.all([H(K(o(e, t), ...s, X("listed", "==", !0), X("isPublished", "==", !0), U(1))), ...i ? [H(K(o(e, t), ...s, X("authorUid", "==", i), U(1)))] : []])).flatMap((e) => e.docs)[0];
573
582
  }
574
583
  //#endregion
575
584
  //#region src/utils/extractEditorImageUrls.ts
576
- function qt(e) {
585
+ function Yt(e) {
577
586
  if (!e) return [];
578
587
  let t = /* @__PURE__ */ new Set();
579
588
  for (let n of e.matchAll(/!\[[^\]]*]\((https?:\/\/[^)\s]+)\)/g)) n[1] && t.add(n[1].replaceAll("&amp;", "&"));
@@ -582,7 +591,7 @@ function qt(e) {
582
591
  }
583
592
  //#endregion
584
593
  //#region src/utils/storagePath.ts
585
- function Jt(e) {
594
+ function Xt(e) {
586
595
  try {
587
596
  let t = new URL(e), n = t.hostname.toLowerCase();
588
597
  if (!(n === "firebasestorage.googleapis.com" || n.endsWith(".firebasestorage.app"))) return null;
@@ -592,107 +601,107 @@ function Jt(e) {
592
601
  return null;
593
602
  }
594
603
  }
595
- function Yt(e) {
604
+ function Zt(e) {
596
605
  return e.match(/(?:^|\/)posts\/([^/]+)\//)?.[1] ?? null;
597
606
  }
598
607
  //#endregion
599
608
  //#region src/utils/deletePostCascade.ts
600
- async function Xt(e) {
609
+ async function Qt(e) {
601
610
  try {
602
611
  let t = await Ke(e);
603
- await Promise.all([...t.items.map((e) => He(e).catch(() => {})), ...t.prefixes.map((e) => Xt(e))]);
612
+ await Promise.all([...t.items.map((e) => He(e).catch(() => {})), ...t.prefixes.map((e) => Qt(e))]);
604
613
  } catch {}
605
614
  }
606
- async function Zt(e, t) {
607
- await Xt(qe(e, `${t}/images`)), await Xt(qe(e, `${t}/attachments`)), await Xt(qe(e, `${t}/comment-images`));
615
+ async function $t(e, t) {
616
+ await Qt(qe(e, `${t}/images`)), await Qt(qe(e, `${t}/attachments`)), await Qt(qe(e, `${t}/comment-images`));
608
617
  }
609
- var Qt = 450;
610
- async function $t(e, t, n) {
618
+ var en = 450;
619
+ async function tn(e, t, n) {
611
620
  return (await H(K(g(e, t), X("postId", "==", n)))).docs.flatMap((e) => ue(e.data().images, `${x(t, n)}/comment-images/`));
612
621
  }
613
- async function en(e, t) {
622
+ async function nn(e, t) {
614
623
  let n = [...new Set([...t].filter(Boolean))];
615
624
  await Promise.all(n.map((t) => He(qe(e, t)).catch(() => {})));
616
625
  }
617
- async function tn(e, t) {
626
+ async function rn(e, t) {
618
627
  let n = await H(t);
619
- for (let t = 0; t < n.docs.length; t += Qt) {
628
+ for (let t = 0; t < n.docs.length; t += en) {
620
629
  let r = Oe(e);
621
- n.docs.slice(t, t + Qt).forEach((e) => r.delete(e.ref)), await r.commit();
630
+ n.docs.slice(t, t + en).forEach((e) => r.delete(e.ref)), await r.commit();
622
631
  }
623
632
  }
624
- async function nn(e, t, n) {
625
- await tn(e, K(b(e, t), X("postId", "==", n)));
633
+ async function an(e, t, n) {
634
+ await rn(e, K(b(e, t), X("postId", "==", n)));
626
635
  }
627
- async function rn(e, t, n) {
628
- await tn(e, K(b(e, t), X("commentId", "==", n), X("target", "==", "comment")));
636
+ async function on(e, t, n) {
637
+ await rn(e, K(b(e, t), X("commentId", "==", n), X("target", "==", "comment")));
629
638
  }
630
- async function an(e, t, n, r) {
639
+ async function sn(e, t, n, r) {
631
640
  let i = y(e, n, r), a = s(e, n, r), [o, c] = await Promise.all([V(i), V(a)]);
632
641
  if (!o.exists()) return;
633
- let l = o.data(), u = c.exists() ? String(c.data().content ?? "") : "", d = await $t(e, n, r), f = /* @__PURE__ */ new Set(), p = /* @__PURE__ */ new Set();
642
+ let l = o.data(), u = c.exists() ? String(c.data().content ?? "") : "", d = await tn(e, n, r), f = /* @__PURE__ */ new Set(), p = /* @__PURE__ */ new Set();
634
643
  for (let e of l.attachments ?? []) {
635
644
  if (!e?.path) continue;
636
645
  f.add(e.path);
637
- let t = Yt(e.path);
646
+ let t = Zt(e.path);
638
647
  t && t !== r && p.add(t);
639
648
  }
640
- for (let e of qt(u)) {
641
- let t = Jt(e);
649
+ for (let e of Yt(u)) {
650
+ let t = Xt(e);
642
651
  if (!t) continue;
643
652
  f.add(t);
644
653
  let n = t.replace(/\/images\/([^/]+)$/, "/images/thumbnails/$1").replace(/\.[^.]+$/, ".jpg");
645
654
  n !== t && n.includes("/images/thumbnails/") && f.add(n);
646
655
  let i = t.match(/^(.*\/images\/)([^/]+)\.[^.]+$/);
647
656
  i && !t.includes("/thumbnails/") && f.add(`${i[1]}thumbnails/${i[2]}.jpg`);
648
- let a = Yt(t);
657
+ let a = Zt(t);
649
658
  a && a !== r && p.add(a);
650
659
  }
651
- await Zt(t, x(n, r));
652
- for (let e of p) await Zt(t, x(n, e));
653
- await en(t, d), await en(t, f), await tn(e, K(g(e, n), X("postId", "==", r))), await nn(e, n, r), await ve(a), await ve(i);
660
+ await $t(t, x(n, r));
661
+ for (let e of p) await $t(t, x(n, e));
662
+ await nn(t, d), await nn(t, f), await rn(e, K(g(e, n), X("postId", "==", r))), await an(e, n, r), await ve(a), await ve(i);
654
663
  }
655
664
  //#endregion
656
665
  //#region src/utils/postBody.ts
657
- function on(e) {
666
+ function cn(e) {
658
667
  return String(e || "").replace(/<style[\s\S]*?<\/style>/gi, " ").replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<img\b[^>]*>/gi, " ").replace(/<iframe\b[^>]*>[\s\S]*?<\/iframe>/gi, " ").replace(/<video\b[\s\S]*?<\/video>/gi, " ").replace(/<audio\b[\s\S]*?<\/audio>/gi, " ").replace(/<[^>]+>/g, " ").replace(/&nbsp;/gi, " ").replace(/&#160;/g, " ").replace(/&[a-z]+;/gi, " ").replace(/&#\d+;/g, " ").replace(/[\u200B-\u200D\uFEFF\u00AD]/g, "").replace(/\s+/g, " ").trim();
659
668
  }
660
- function sn(e, t = 40) {
661
- let n = on(e);
669
+ function ln(e, t = 40) {
670
+ let n = cn(e);
662
671
  if (!n) return "";
663
672
  if (n.length <= t) return n;
664
673
  let r = n.slice(0, t), i = r.lastIndexOf(" ");
665
674
  return `${(i >= Math.floor(t * .6) ? r.slice(0, i) : r).trimEnd()}…`;
666
675
  }
667
- function cn(e) {
668
- return on(e).length > 0;
676
+ function un(e) {
677
+ return cn(e).length > 0;
669
678
  }
670
- function ln(e, t) {
679
+ function dn(e, t) {
671
680
  return /<img\b/i.test(String(e || "")) ? !0 : (t || []).some((e) => !!e?.type?.startsWith("image/"));
672
681
  }
673
- function un(e) {
674
- return !cn(e);
682
+ function fn(e) {
683
+ return !un(e);
675
684
  }
676
685
  //#endregion
677
686
  //#region src/utils/postPreview.ts
678
- function dn(e) {
687
+ function pn(e) {
679
688
  return e.replace(/&amp;/g, "&").replace(/&quot;/g, "\"").replace(/&#39;/g, "'");
680
689
  }
681
- function fn(e) {
690
+ function mn(e) {
682
691
  if (e) try {
683
- let t = new URL(dn(e)), n = t.hostname.toLowerCase().replace(/^www\./, "");
692
+ let t = new URL(pn(e)), n = t.hostname.toLowerCase().replace(/^www\./, "");
684
693
  if (n === "youtu.be") return t.pathname.split("/").filter(Boolean)[0];
685
694
  if (n.endsWith("youtube.com") || n === "youtube-nocookie.com") return t.pathname.startsWith("/embed/") || t.pathname.startsWith("/shorts/") ? t.pathname.split("/")[2] : t.searchParams.get("v") || void 0;
686
695
  } catch {
687
696
  return;
688
697
  }
689
698
  }
690
- function pn(e) {
691
- let t = fn(e.videoUrl);
699
+ function hn(e) {
700
+ let t = mn(e.videoUrl);
692
701
  return t ? `https://i.ytimg.com/vi/${t}/hqdefault.jpg` : e.previewImage || e.attachments?.find((e) => e.type.startsWith("image/"))?.url;
693
702
  }
694
- function mn(e, t = []) {
695
- let n = e.match(/<img\b[^>]*\bsrc=["']([^"']+)["']/i), r = e.match(/<iframe\b[^>]*\bsrc=["']([^"']+)["']/i), i = t.find((e) => e.type.startsWith("image/"))?.url, a = t.find((e) => e.type.startsWith("video/"))?.url, o = r ? dn(r[1]) : a, s = o ? fn(o) : void 0, c = n ? dn(n[1]) : i || (s ? `https://i.ytimg.com/vi/${s}/hqdefault.jpg` : void 0);
703
+ function gn(e, t = []) {
704
+ let n = e.match(/<img\b[^>]*\bsrc=["']([^"']+)["']/i), r = e.match(/<iframe\b[^>]*\bsrc=["']([^"']+)["']/i), i = t.find((e) => e.type.startsWith("image/"))?.url, a = t.find((e) => e.type.startsWith("video/"))?.url, o = r ? pn(r[1]) : a, s = o ? mn(o) : void 0, c = n ? pn(n[1]) : i || (s ? `https://i.ytimg.com/vi/${s}/hqdefault.jpg` : void 0);
696
705
  return {
697
706
  summary: e.replace(/<style[\s\S]*?<\/style>/gi, " ").replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<br\s*\/?>/gi, "\n").replace(/<\/(p|div|li|h[1-6]|blockquote)>/gi, "\n").replace(/<[^>]+>/g, " ").replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/[^\S\n]+/g, " ").replace(/ *\n */g, "\n").replace(/\n{2,}/g, "\n").trim().slice(0, 160),
698
707
  ...c ? { previewImage: c } : {},
@@ -701,10 +710,10 @@ function mn(e, t = []) {
701
710
  }
702
711
  //#endregion
703
712
  //#region src/utils/saveBoardVisibility.ts
704
- function hn(e) {
713
+ function _n(e) {
705
714
  return e === "hidden" ? "hidden" : "public";
706
715
  }
707
- async function gn(e, t, n, r) {
716
+ async function vn(e, t, n, r) {
708
717
  let i;
709
718
  for (;;) {
710
719
  let a = await H(K(t, X("boardId", "==", n), G("__name__"), ...i ? [J(i)] : [], U(400)));
@@ -714,20 +723,20 @@ async function gn(e, t, n, r) {
714
723
  if (s && await o.commit(), i = a.docs[a.docs.length - 1], a.docs.length < 400) break;
715
724
  }
716
725
  }
717
- async function _n(e, t, n, r) {
718
- await gn(e, o(e, t), n, r), await gn(e, g(e, t), n, r);
726
+ async function yn(e, t, n, r) {
727
+ await vn(e, o(e, t), n, r), await vn(e, g(e, t), n, r);
719
728
  }
720
- async function vn(e, t, n, r) {
721
- let i = w(e, t, n), a = hn(r.visibility);
729
+ async function bn(e, t, n, r) {
730
+ let i = w(e, t, n), a = _n(r.visibility);
722
731
  await Ee(e, async (e) => {
723
732
  let t = await e.get(i), n = t.data(), o = n?.visibilityTransition || "";
724
733
  if (o && o !== a) throw Error("Finish the pending visibility change before changing it again.");
725
- return !t.exists() || !o && hn(n?.visibility) === a ? (e.set(i, r, { merge: !0 }), !1) : (e.set(i, {
734
+ return !t.exists() || !o && _n(n?.visibility) === a ? (e.set(i, r, { merge: !0 }), !1) : (e.set(i, {
726
735
  ...r,
727
- visibility: a === "public" ? "public" : hn(n?.visibility),
736
+ visibility: a === "public" ? "public" : _n(n?.visibility),
728
737
  visibilityTransition: a
729
738
  }, { merge: !0 }), !0);
730
- }) && (await _n(e, t, n, a !== "hidden"), await Ee(e, async (e) => {
739
+ }) && (await yn(e, t, n, a !== "hidden"), await Ee(e, async (e) => {
731
740
  if ((await e.get(i)).data()?.visibilityTransition !== a) throw Error("The visibility transition changed. Reload and retry.");
732
741
  e.update(i, {
733
742
  visibility: a,
@@ -738,7 +747,7 @@ async function vn(e, t, n, r) {
738
747
  }
739
748
  //#endregion
740
749
  //#region src/utils/moderation-prompt.ts
741
- var yn = "당신은 게시판 콘텐츠를 심사하는 엄격한 모더레이터입니다.\n아래 텍스트에 욕설·비속어·성희롱·혐오·위협·스팸·도배성 광고가 있으면 반드시 거부(flagged=true)합니다.\n\nflagged=true 예:\n- 욕설/비속어: 씨발, 시발, 병신, 지랄, 좆, 개새끼, 존나, fuck, shit, bitch 등\n- 성적 모욕, 혐오 발언, 협박\n- 게시판 주제와 무관한 도배성 광고/스팸\n\nflagged=false:\n- 게시판 주제에 맞는 정상적인 글/댓글 (강한 의견이나 정당한 비판은 허용)\n\nJSON만 출력:\n{\"flagged\":boolean,\"category\":\"none\"|\"abuse\"|\"spam\"|\"adult\"|\"violence\"|\"other\",\"reason\":string}\n- flagged=true면 category를 고르고, reason은 한국어로 **무엇이 문제인지** 구체적으로 1문장\n (예: \"욕설(비속어)이 포함되어 게시할 수 없습니다.\", \"광고·스팸성 문구가 있어 게시할 수 없습니다.\")\n 욕설 단어 전체를 그대로 길게 인용하지 말고, 유형(욕설/혐오/스팸 등)을 분명히 밝힌다.\n- flagged=false면 category는 \"none\", reason은 \"\"", bn = {
750
+ var xn = "당신은 게시판 콘텐츠를 심사하는 엄격한 모더레이터입니다.\n아래 텍스트에 욕설·비속어·성희롱·혐오·위협·스팸·도배성 광고가 있으면 반드시 거부(flagged=true)합니다.\n\nflagged=true 예:\n- 욕설/비속어: 씨발, 시발, 병신, 지랄, 좆, 개새끼, 존나, fuck, shit, bitch 등\n- 성적 모욕, 혐오 발언, 협박\n- 게시판 주제와 무관한 도배성 광고/스팸\n\nflagged=false:\n- 게시판 주제에 맞는 정상적인 글/댓글 (강한 의견이나 정당한 비판은 허용)\n\nJSON만 출력:\n{\"flagged\":boolean,\"category\":\"none\"|\"abuse\"|\"spam\"|\"adult\"|\"violence\"|\"other\",\"reason\":string}\n- flagged=true면 category를 고르고, reason은 한국어로 **무엇이 문제인지** 구체적으로 1문장\n (예: \"욕설(비속어)이 포함되어 게시할 수 없습니다.\", \"광고·스팸성 문구가 있어 게시할 수 없습니다.\")\n 욕설 단어 전체를 그대로 길게 인용하지 말고, 유형(욕설/혐오/스팸 등)을 분명히 밝힌다.\n- flagged=false면 category는 \"none\", reason은 \"\"", Sn = {
742
751
  ko: "Korean",
743
752
  en: "English",
744
753
  ja: "Japanese",
@@ -750,12 +759,12 @@ var yn = "당신은 게시판 콘텐츠를 심사하는 엄격한 모더레이
750
759
  ar: "Arabic",
751
760
  id: "Indonesian"
752
761
  };
753
- function xn(e) {
754
- let t = bn[e] ?? "English", n = fe(e).name;
762
+ function Cn(e) {
763
+ let t = Sn[e] ?? "English", n = fe(e).name;
755
764
  return t === n ? t : `${t} (${n})`;
756
765
  }
757
- function Sn(e) {
758
- let t = xn(e);
766
+ function wn(e) {
767
+ let t = Cn(e);
759
768
  return `You are a strict moderator reviewing content for a message board.
760
769
  If the text below contains profanity, vulgar slang, sexual harassment, hate speech, threats, spam, or repetitive/flooding advertisements, you MUST reject it (flagged=true).
761
770
 
@@ -782,17 +791,17 @@ Output JSON only:
782
791
  Do not quote the offensive words at length; clearly state the type (profanity/hate/spam, etc.).
783
792
  - If flagged=false, category is "none" and reason is ""`;
784
793
  }
785
- function Cn(e = "ko") {
786
- return e === "ko" ? yn : Sn(e);
794
+ function Tn(e = "ko") {
795
+ return e === "ko" ? xn : wn(e);
787
796
  }
788
- function wn(e, t = "ko") {
797
+ function En(e, t = "ko") {
789
798
  let n = `"""${e.slice(0, 4e3)}"""`;
790
799
  return t === "ko" ? `심사할 텍스트:\n${n}` : `Text to review:\n${n}`;
791
800
  }
792
- function Tn(e = "ko") {
801
+ function Dn(e = "ko") {
793
802
  return e === "ko" ? "이 이미지를 심사해 주세요." : "Please review this image.";
794
803
  }
795
- var En = {
804
+ var On = {
796
805
  abuse: "욕설·비속어·모욕",
797
806
  spam: "스팸·광고",
798
807
  adult: "선정적·성인 내용",
@@ -800,10 +809,10 @@ var En = {
800
809
  other: "부적절한 내용",
801
810
  none: ""
802
811
  };
803
- function Dn(e, t = "ko") {
804
- return !e || e === "none" ? "" : e in En ? A(t, `moderationPrompt.categories.${e}`) : e;
812
+ function kn(e, t = "ko") {
813
+ return !e || e === "none" ? "" : e in On ? A(t, `moderationPrompt.categories.${e}`) : e;
805
814
  }
806
- function On(e) {
815
+ function An(e) {
807
816
  let t = e.locale ?? "ko";
808
817
  if (e.via === "local" && e.localHit) {
809
818
  let n = e.localHit.trim();
@@ -811,10 +820,10 @@ function On(e) {
811
820
  }
812
821
  let n = (e.reason || "").trim();
813
822
  if (n) return n;
814
- let r = e.category ? Dn(e.category, t) : "";
823
+ let r = e.category ? kn(e.category, t) : "";
815
824
  return r ? A(t, "moderationPrompt.categoryBlocked", { category: r }) : A(t, "moderationPrompt.genericBlocked");
816
825
  }
817
- var kn = [
826
+ var jn = [
818
827
  "씨발",
819
828
  "시발",
820
829
  "시이발",
@@ -840,10 +849,10 @@ var kn = [
840
849
  "bitch",
841
850
  "asshole"
842
851
  ];
843
- function An(e) {
852
+ function Mn(e) {
844
853
  return new RegExp(e.join("|"), "i");
845
854
  }
846
- var jn = /* @__PURE__ */ new Set([
855
+ var Nn = /* @__PURE__ */ new Set([
847
856
  "none",
848
857
  "abuse",
849
858
  "spam",
@@ -851,7 +860,7 @@ var jn = /* @__PURE__ */ new Set([
851
860
  "violence",
852
861
  "other"
853
862
  ]);
854
- function Mn(e) {
863
+ function Pn(e) {
855
864
  let t = e.trim();
856
865
  if (!t) return null;
857
866
  let n;
@@ -869,7 +878,7 @@ function Mn(e) {
869
878
  if (typeof n != "object" || !n) return null;
870
879
  let r = n;
871
880
  if (typeof r.flagged != "boolean") return null;
872
- let i = typeof r.category == "string" && jn.has(r.category) ? r.category : r.flagged ? "other" : "none", a = typeof r.reason == "string" ? r.reason : "";
881
+ let i = typeof r.category == "string" && Nn.has(r.category) ? r.category : r.flagged ? "other" : "none", a = typeof r.reason == "string" ? r.reason : "";
873
882
  return {
874
883
  flagged: r.flagged,
875
884
  category: i,
@@ -878,11 +887,11 @@ function Mn(e) {
878
887
  }
879
888
  //#endregion
880
889
  //#region src/composables/useMemiBoardAutoTranslate.ts
881
- var Nn = 3e4, Pn = 45e3, Fn = 12e3, In = 3;
882
- function Ln(e) {
890
+ var Fn = 3e4, In = 45e3, Ln = 12e3, Rn = 3;
891
+ function zn(e) {
883
892
  return e.trim().replace(/^```(?:html|text)?\s*/i, "").replace(/\s*```$/, "").trim();
884
893
  }
885
- async function Rn(e, t, n) {
894
+ async function Bn(e, t, n) {
886
895
  let r = Array(e.length), i = 0;
887
896
  async function a() {
888
897
  for (; i < e.length;) {
@@ -892,7 +901,7 @@ async function Rn(e, t, n) {
892
901
  }
893
902
  return await Promise.all(Array.from({ length: Math.min(t, e.length) }, () => a())), r;
894
903
  }
895
- function zn(e, t) {
904
+ function Vn(e, t) {
896
905
  return new Promise((n, r) => {
897
906
  let i = setTimeout(() => r(/* @__PURE__ */ Error(`translation timed out after ${t / 1e3}s`)), t);
898
907
  e.then((e) => {
@@ -902,7 +911,7 @@ function zn(e, t) {
902
911
  });
903
912
  });
904
913
  }
905
- function Bn() {
914
+ function Hn() {
906
915
  let e = Me(), { moderation: t } = v();
907
916
  function n(n, r) {
908
917
  let i = Xe(e, {
@@ -928,12 +937,12 @@ function Bn() {
928
937
  async function r(e, t, r, i) {
929
938
  let a = Object.keys(e.fields), o = [
930
939
  `Purpose: ${i}`,
931
- `Source language: ${xn(t)} (${t})`,
932
- `Translate into: ${r.map((e) => `${e} = ${xn(e)}`).join(", ")}`,
940
+ `Source language: ${Cn(t)} (${t})`,
941
+ `Translate into: ${r.map((e) => `${e} = ${Cn(e)}`).join(", ")}`,
933
942
  e.context ? `Context (do not translate): ${e.context}` : "",
934
943
  "Text (JSON):",
935
944
  JSON.stringify(e.fields)
936
- ].filter(Boolean).join("\n"), s = await zn(n(r, a).generateContent(o), Nn), c = JSON.parse(s.response.text()), l = {};
945
+ ].filter(Boolean).join("\n"), s = await Vn(n(r, a).generateContent(o), Fn), c = JSON.parse(s.response.text()), l = {};
937
946
  for (let e of r) {
938
947
  let t = c?.[e];
939
948
  if (!t) continue;
@@ -979,14 +988,14 @@ function Bn() {
979
988
  D();
980
989
  let n = t.source ?? "ko", r = (t.targets ?? T.map((e) => e.code)).filter((e) => e !== n), i = e.trim();
981
990
  if (!i || !r.length) return {};
982
- if (i.length > 12e3) throw Error(`translation is limited to ${Fn} characters`);
983
- let o = await Rn(r, In, async (e) => {
991
+ if (i.length > 12e3) throw Error(`translation is limited to ${Ln} characters`);
992
+ let o = await Bn(r, Rn, async (e) => {
984
993
  let t = [
985
- `Source language: ${xn(n)} (${n})`,
986
- `Translate into: ${e} = ${xn(e)}`,
994
+ `Source language: ${Cn(n)} (${n})`,
995
+ `Translate into: ${e} = ${Cn(e)}`,
987
996
  "HTML:",
988
997
  i
989
- ].join("\n"), r = Ln((await zn(a().generateContent(t), Pn)).response.text());
998
+ ].join("\n"), r = zn((await Vn(a().generateContent(t), In)).response.text());
990
999
  if (!r) throw Error(`empty translation for ${e}`);
991
1000
  return [e, r];
992
1001
  });
@@ -999,7 +1008,7 @@ function Bn() {
999
1008
  }
1000
1009
  //#endregion
1001
1010
  //#region src/composables/useMemiBoardSettings.ts
1002
- var Vn = [
1011
+ var Un = [
1003
1012
  {
1004
1013
  id: "free",
1005
1014
  label: "자유",
@@ -1027,17 +1036,17 @@ var Vn = [
1027
1036
  visibility: "public",
1028
1037
  order: 2
1029
1038
  }
1030
- ], Hn = Vn;
1031
- function Un(e) {
1039
+ ], Wn = Un;
1040
+ function Gn(e) {
1032
1041
  return e === "hidden" ? "hidden" : "public";
1033
1042
  }
1034
- function Wn(e, t, n) {
1043
+ function Kn(e, t, n) {
1035
1044
  return {
1036
1045
  id: e,
1037
1046
  label: typeof t.label == "string" && t.label.trim() ? t.label.trim() : e,
1038
1047
  order: typeof t.order == "number" ? t.order : n,
1039
1048
  description: (typeof t.description == "string" ? t.description : "") || "",
1040
- visibility: Un(t.visibility),
1049
+ visibility: Gn(t.visibility),
1041
1050
  visibilityTransition: t.visibilityTransition === "hidden" || t.visibilityTransition === "public" ? t.visibilityTransition : void 0,
1042
1051
  listView: t.listView ?? "default",
1043
1052
  editorType: t.editorType ?? (t.listView === "image" ? "image" : "default"),
@@ -1048,22 +1057,23 @@ function Wn(e, t, n) {
1048
1057
  locale: N(t.locale) ?? "",
1049
1058
  autoTranslate: t.autoTranslate === !0,
1050
1059
  autoTranslatePosts: t.autoTranslatePosts === !0,
1060
+ translationLocales: mt(t.translationLocales),
1051
1061
  i18n: Q(t.i18n),
1052
1062
  i18nSource: ut(t.i18nSource)
1053
1063
  };
1054
1064
  }
1055
- function Gn() {
1056
- let e = () => M(), t = Z(), r = Me(), { isSignedIn: i, isAdmin: a, isStaff: s, user: c } = Vt(), { t: l, locale: u } = j(), { translateEntries: d } = Bn(), f = P(() => K(S(t, e()), G("order", "asc"))), { data: m, pending: h } = ke(f, { ssrKey: p(e(), "boards") }), g = P(() => m.value.map((e, t) => Wn(e.id, e, t))), _ = g, v = P(() => g.value.filter((e) => e.visibility !== "hidden")), y = v;
1065
+ function qn() {
1066
+ let e = () => M(), t = Z(), r = Me(), { isSignedIn: i, isAdmin: a, isStaff: s, user: c } = Ut(), { t: l, locale: u } = j(), { translateEntries: d } = Hn(), f = P(() => K(S(t, e()), G("order", "asc"))), { data: m, pending: h } = ke(f, { ssrKey: p(e(), "boards") }), g = P(() => m.value.map((e, t) => Kn(e.id, e, t))), _ = g, v = P(() => g.value.filter((e) => e.visibility !== "hidden")), y = v;
1057
1067
  function b(e, t) {
1058
1068
  if (!e) return;
1059
1069
  let n = g.value.find((t) => t.id === e);
1060
- return n ? ht(n.i18n, n.i18nSource, "label", n.label, t ?? u.value) || n.label : e;
1070
+ return n ? _t(n.i18n, n.i18nSource, "label", n.label, t ?? u.value) || n.label : e;
1061
1071
  }
1062
1072
  let x = b;
1063
1073
  function C(e, t) {
1064
1074
  if (!e) return;
1065
1075
  let n = g.value.find((t) => t.id === e), r = n?.description?.trim();
1066
- if (n && r) return ht(n.i18n, n.i18nSource, "description", r, t ?? u.value) || void 0;
1076
+ if (n && r) return _t(n.i18n, n.i18nSource, "description", r, t ?? u.value) || void 0;
1067
1077
  }
1068
1078
  let T = C;
1069
1079
  function D(e) {
@@ -1077,16 +1087,16 @@ function Gn() {
1077
1087
  function k(e) {
1078
1088
  if (e) return g.value.find((t) => t.id === e);
1079
1089
  }
1080
- At((e) => k(e));
1090
+ Mt((e) => k(e));
1081
1091
  function ne(e) {
1082
- return Dt(c.value?.uid, a.value, s.value, e ? k(e) : void 0);
1092
+ return kt(c.value?.uid, a.value, s.value, e ? k(e) : void 0);
1083
1093
  }
1084
1094
  let re = P(() => g.value.filter((e) => ne(e.id)));
1085
1095
  function ie(e, t) {
1086
1096
  return {
1087
1097
  label: e.label.trim(),
1088
1098
  description: (e.description ?? "").trim(),
1089
- visibility: Un(e.visibility),
1099
+ visibility: Gn(e.visibility),
1090
1100
  listView: e.listView ?? "default",
1091
1101
  editorType: e.editorType ?? (e.listView === "image" ? "image" : "default"),
1092
1102
  writeRole: e.writeRole ?? "user",
@@ -1096,6 +1106,7 @@ function Gn() {
1096
1106
  locale: N(e.locale) ?? "",
1097
1107
  autoTranslate: e.autoTranslate === !0,
1098
1108
  autoTranslatePosts: e.autoTranslatePosts === !0,
1109
+ translationLocales: mt(e.translationLocales),
1099
1110
  ...e.i18n ? { i18n: e.i18n } : {},
1100
1111
  ...e.i18nSource ? { i18nSource: e.i18nSource } : {},
1101
1112
  order: t,
@@ -1106,7 +1117,7 @@ function Gn() {
1106
1117
  if (!n()) return e;
1107
1118
  let t = e.label.trim(), r = (e.description ?? "").trim();
1108
1119
  if (!t) return e;
1109
- if (yt({
1120
+ if (xt({
1110
1121
  ...e,
1111
1122
  label: t,
1112
1123
  description: r
@@ -1115,22 +1126,23 @@ function Gn() {
1115
1126
  label: t,
1116
1127
  description: r
1117
1128
  };
1118
- let i = u.value, a = { label: t };
1119
- r && (a.description = r);
1120
- let o = ft(i, a, (await d([{
1129
+ let i = u.value, a = pt(e), o = { label: t };
1130
+ r && (o.description = r);
1131
+ let s = ft(i, o, (await d([{
1121
1132
  id: e.id || "board",
1122
- fields: a,
1133
+ fields: o,
1123
1134
  context: e.id
1124
1135
  }], {
1125
1136
  source: i,
1137
+ targets: a,
1126
1138
  purpose: "board name and short description shown in navigation and page headers"
1127
1139
  }))[e.id || "board"]);
1128
- if (!pt(o, Object.keys(a))) throw Error(l("settings.autoTranslate.failed"));
1140
+ if (!ht(s, Object.keys(o), [...a, i])) throw Error(l("settings.autoTranslate.failed", { count: a.length }));
1129
1141
  return {
1130
1142
  ...e,
1131
1143
  label: t,
1132
1144
  description: r,
1133
- i18n: o,
1145
+ i18n: s,
1134
1146
  i18nSource: {
1135
1147
  locale: i,
1136
1148
  label: t,
@@ -1142,12 +1154,12 @@ function Gn() {
1142
1154
  if (!i.value) throw Error(l("boardSettings.signInRequired"));
1143
1155
  let a = n.id.trim(), o = n.label.trim();
1144
1156
  if (!a || !o) throw Error(l("boardSettings.idAndNameRequired"));
1145
- let s = Un(n.visibility), c = ie({
1157
+ let s = Gn(n.visibility), c = ie({
1146
1158
  ...n,
1147
1159
  label: o,
1148
1160
  visibility: s
1149
1161
  }, r);
1150
- await vn(t, e(), a, c);
1162
+ await bn(t, e(), a, c);
1151
1163
  }
1152
1164
  let oe = A;
1153
1165
  async function se(e) {
@@ -1163,7 +1175,7 @@ function Gn() {
1163
1175
  for (;;) {
1164
1176
  let n = await H(K(o(t, e()), X("boardId", "==", a), U(25)));
1165
1177
  if (n.empty) break;
1166
- for (let i of n.docs) await an(t, We(r), e(), i.id);
1178
+ for (let i of n.docs) await sn(t, We(r), e(), i.id);
1167
1179
  }
1168
1180
  await ve(w(t, e(), n));
1169
1181
  }
@@ -1217,25 +1229,25 @@ function Gn() {
1217
1229
  saveCategories: ce,
1218
1230
  deleteBoard: le,
1219
1231
  deleteCategory: ue,
1220
- DEFAULT_BOARDS: Vn,
1221
- DEFAULT_CATEGORIES: Hn
1232
+ DEFAULT_BOARDS: Un,
1233
+ DEFAULT_CATEGORIES: Wn
1222
1234
  };
1223
1235
  }
1224
1236
  //#endregion
1225
1237
  //#region src/composables/useMemiBoardPosts.ts
1226
- var Kn = /* @__PURE__ */ new Map();
1227
- function qn(e) {
1238
+ var Jn = /* @__PURE__ */ new Map();
1239
+ function Yn(e) {
1228
1240
  if (!e || typeof e != "object") return !1;
1229
1241
  let t = "code" in e ? String(e.code) : "";
1230
1242
  return t === "permission-denied" || t === "firestore/permission-denied";
1231
1243
  }
1232
- function Jn(e, t) {
1244
+ function Xn(e, t) {
1233
1245
  let n = L(e)?.trim();
1234
1246
  if (!n) throw Error(A(t, "posts.boardIdRequired"));
1235
1247
  return n;
1236
1248
  }
1237
- function Yn(e) {
1238
- let t = Z(), n = Me(), { isBoardHidden: r, getBoard: i } = Gn(), { user: a, canManageBoard: c } = Vt(), { t: l, locale: u } = j(), d = () => M(), f = () => Jn(e, u.value), p = () => o(t, d()), m = (e) => y(t, d(), e), h = (e) => s(t, d(), e);
1249
+ function Zn(e) {
1250
+ let t = Z(), n = Me(), { isBoardHidden: r, getBoard: i } = qn(), { user: a, canManageBoard: c } = Ut(), { t: l, locale: u } = j(), d = () => M(), f = () => Xn(e, u.value), p = () => o(t, d()), m = (e) => y(t, d(), e), h = (e) => s(t, d(), e);
1239
1251
  function g(e, t, n) {
1240
1252
  return {
1241
1253
  id: e,
@@ -1254,7 +1266,7 @@ function Yn(e) {
1254
1266
  return i(f())?.editorType === "image";
1255
1267
  }
1256
1268
  function b(e) {
1257
- let t = v(), n = cn(e.content), r = ln(e.content, e.attachments);
1269
+ let t = v(), n = un(e.content), r = dn(e.content, e.attachments);
1258
1270
  if (t) {
1259
1271
  if (!r) throw Error(l("posts.photoRequired"));
1260
1272
  return;
@@ -1266,7 +1278,7 @@ function Yn(e) {
1266
1278
  return z(p()).id;
1267
1279
  }
1268
1280
  function S(e) {
1269
- return Kt(t, d(), f(), e, a.value?.uid, c(f()));
1281
+ return Jt(t, d(), f(), e, a.value?.uid, c(f()));
1270
1282
  }
1271
1283
  async function C(e = {}) {
1272
1284
  let t = e.pageSize ?? 20, n = [X("boardId", "==", f()), X("isPublished", "==", !0)];
@@ -1296,20 +1308,20 @@ function Yn(e) {
1296
1308
  post: e
1297
1309
  } : { status: "not-found" };
1298
1310
  } catch (e) {
1299
- return qn(e) ? { status: "permission-denied" } : {
1311
+ return Yn(e) ? { status: "permission-denied" } : {
1300
1312
  status: "error",
1301
1313
  message: e instanceof Error ? e.message : String(e)
1302
1314
  };
1303
1315
  }
1304
1316
  } catch (e) {
1305
- return qn(e) ? { status: "permission-denied" } : {
1317
+ return Yn(e) ? { status: "permission-denied" } : {
1306
1318
  status: "error",
1307
1319
  message: e instanceof Error ? e.message : String(e)
1308
1320
  };
1309
1321
  }
1310
1322
  }
1311
1323
  async function ee(e) {
1312
- let t = e.trim(), n = Kn.get(`${f()}/${t}`);
1324
+ let t = e.trim(), n = Jn.get(`${f()}/${t}`);
1313
1325
  if (n) return {
1314
1326
  status: "ok",
1315
1327
  id: n
@@ -1321,7 +1333,7 @@ function Yn(e) {
1321
1333
  id: e.id
1322
1334
  } : { status: r(f()) ? "permission-denied" : "not-found" };
1323
1335
  } catch (e) {
1324
- return qn(e) ? { status: "permission-denied" } : {
1336
+ return Yn(e) ? { status: "permission-denied" } : {
1325
1337
  status: "error",
1326
1338
  message: e instanceof Error ? e.message : String(e)
1327
1339
  };
@@ -1362,8 +1374,8 @@ function Yn(e) {
1362
1374
  async function k(e) {
1363
1375
  b(e);
1364
1376
  let n = e.postId || x(), r = v(), i, a;
1365
- r ? (i = n, a = sn(e.content)) : (a = e.title.trim(), i = e.slug?.trim() || await O(a));
1366
- let o = mn(e.content, e.attachments), s = _(), c = Oe(t);
1377
+ r ? (i = n, a = ln(e.content)) : (a = e.title.trim(), i = e.slug?.trim() || await O(a));
1378
+ let o = gn(e.content, e.attachments), s = _(), c = Oe(t);
1367
1379
  return c.set(m(n), {
1368
1380
  boardId: f(),
1369
1381
  slug: i,
@@ -1393,11 +1405,11 @@ function Yn(e) {
1393
1405
  i18n: e.translations.contentI18n,
1394
1406
  i18nSource: e.translations.contentI18nSource
1395
1407
  } : {}
1396
- }), await c.commit(), Kn.set(`${f()}/${i}`, n), i;
1408
+ }), await c.commit(), Jn.set(`${f()}/${i}`, n), i;
1397
1409
  }
1398
1410
  async function ne(e, t) {
1399
1411
  b(t);
1400
- let n = v() ? sn(t.content) : t.title.trim(), r = mn(t.content, t.attachments), i = _();
1412
+ let n = v() ? ln(t.content) : t.title.trim(), r = gn(t.content, t.attachments), i = _();
1401
1413
  await Promise.all([Y(m(e), {
1402
1414
  title: n,
1403
1415
  ...r,
@@ -1452,8 +1464,8 @@ function Yn(e) {
1452
1464
  });
1453
1465
  }
1454
1466
  async function A(e) {
1455
- await an(t, We(n), d(), e);
1456
- for (let [t, n] of Kn) n === e && Kn.delete(t);
1467
+ await sn(t, We(n), d(), e);
1468
+ for (let [t, n] of Jn) n === e && Jn.delete(t);
1457
1469
  }
1458
1470
  return {
1459
1471
  createPostId: x,
@@ -1471,9 +1483,9 @@ function Yn(e) {
1471
1483
  deletePost: A
1472
1484
  };
1473
1485
  }
1474
- var Xn = 10, Zn = 10;
1475
- function Qn(e, t = {}) {
1476
- let n = Z(), { isAdmin: r, user: i, canManageBoard: a } = Vt(), s = () => M(), c = t.pageSize ?? Xn, l = P(() => i.value?.uid ?? ""), u = P(() => Jn(e)), d = () => o(n, s()), f = P(() => t.publicOnly === void 0 ? !(r.value || a(u.value)) : typeof t.publicOnly == "boolean" ? t.publicOnly : t.publicOnly.value), p = I([]), m = I([]), h = I([]), g = I([]), _ = I(!0), v = I(!0), y = I(!0), b = I(!1), x = I(""), S = null, C = null, w, T, E;
1486
+ var Qn = 10, $n = 10;
1487
+ function er(e, t = {}) {
1488
+ let n = Z(), { isAdmin: r, user: i, canManageBoard: a } = Ut(), s = () => M(), c = t.pageSize ?? Qn, l = P(() => i.value?.uid ?? ""), u = P(() => Xn(e)), d = () => o(n, s()), f = P(() => t.publicOnly === void 0 ? !(r.value || a(u.value)) : typeof t.publicOnly == "boolean" ? t.publicOnly : t.publicOnly.value), p = I([]), m = I([]), h = I([]), g = I([]), _ = I(!0), v = I(!0), y = I(!0), b = I(!1), x = I(""), S = null, C = null, w, T, E;
1477
1489
  function D(e) {
1478
1490
  return e.createdAt?.toMillis?.() ?? 0;
1479
1491
  }
@@ -1501,7 +1513,7 @@ function Qn(e, t = {}) {
1501
1513
  });
1502
1514
  }
1503
1515
  function k() {
1504
- E?.(), E = W(K(d(), ...O(), X("pinned", "==", !0), G("pinnedAt", "desc"), U(Zn)), (e) => {
1516
+ E?.(), E = W(K(d(), ...O(), X("pinned", "==", !0), G("pinnedAt", "desc"), U($n)), (e) => {
1505
1517
  g.value = e.docs.map((e) => ({
1506
1518
  id: e.id,
1507
1519
  ...e.data()
@@ -1567,8 +1579,8 @@ function Qn(e, t = {}) {
1567
1579
  loadMore: re
1568
1580
  };
1569
1581
  }
1570
- function $n(e, t) {
1571
- let n = Z(), r = () => M(), i = P(() => Jn(e)), a = P(() => typeof t == "string" ? t : t.value), o = P(() => y(n, r(), a.value)), c = P(() => s(n, r(), a.value)), { data: l, pending: u } = je(o), { data: d, pending: f } = je(c);
1582
+ function tr(e, t) {
1583
+ let n = Z(), r = () => M(), i = P(() => Xn(e)), a = P(() => typeof t == "string" ? t : t.value), o = P(() => y(n, r(), a.value)), c = P(() => s(n, r(), a.value)), { data: l, pending: u } = je(o), { data: d, pending: f } = je(c);
1572
1584
  return {
1573
1585
  post: P(() => {
1574
1586
  if (!l.value || l.value.boardId !== i.value) return null;
@@ -1588,9 +1600,9 @@ function $n(e, t) {
1588
1600
  }
1589
1601
  //#endregion
1590
1602
  //#region src/composables/useMemiBoardUserPosts.ts
1591
- var er = 10;
1592
- function tr(e, t = {}) {
1593
- let n = Z(), r = () => M(), i = () => o(n, r()), a = t.pageSize ?? er, s = P(() => L(e)?.trim() || ""), c = I([]), l = I([]), u = I(!0), d = I(!0), f = I(!1), p = I(""), m = null, h = null, g;
1603
+ var nr = 10;
1604
+ function rr(e, t = {}) {
1605
+ let n = Z(), r = () => M(), i = () => o(n, r()), a = t.pageSize ?? nr, s = P(() => L(e)?.trim() || ""), c = I([]), l = I([]), u = I(!0), d = I(!0), f = I(!1), p = I(""), m = null, h = null, g;
1594
1606
  function _(e) {
1595
1607
  return {
1596
1608
  id: e.id,
@@ -1656,9 +1668,9 @@ function tr(e, t = {}) {
1656
1668
  }
1657
1669
  //#endregion
1658
1670
  //#region src/composables/useMemiBoardUserComments.ts
1659
- var nr = 10;
1660
- function rr(e, t = {}) {
1661
- let n = Z(), r = () => M(), i = () => g(n, r()), a = t.pageSize ?? nr, o = P(() => L(e)?.trim() || ""), s = I([]), c = I([]), l = I(!0), u = I(!0), d = I(!1), f = I(""), p = null, m = null, h;
1671
+ var ir = 10;
1672
+ function ar(e, t = {}) {
1673
+ let n = Z(), r = () => M(), i = () => g(n, r()), a = t.pageSize ?? ir, o = P(() => L(e)?.trim() || ""), s = I([]), c = I([]), l = I(!0), u = I(!0), d = I(!1), f = I(""), p = null, m = null, h;
1662
1674
  function _(e) {
1663
1675
  return {
1664
1676
  id: e.id,
@@ -1720,11 +1732,11 @@ function rr(e, t = {}) {
1720
1732
  }
1721
1733
  //#endregion
1722
1734
  //#region src/composables/useMemiBoardCommentFeed.ts
1723
- var ir = 10;
1724
- function ar(e = "latest", t = {}) {
1735
+ var or = 10;
1736
+ function sr(e = "latest", t = {}) {
1725
1737
  let n = Z(), r = () => M(), i = () => g(n, r()), a = P(() => {
1726
1738
  let e = Math.floor(Number(L(t.pageSize)));
1727
- return Number.isFinite(e) && e > 0 ? e : ir;
1739
+ return Number.isFinite(e) && e > 0 ? e : or;
1728
1740
  }), o = P(() => L(e) === "likes" ? "likes" : "latest"), s = I([]), c = I([]), l = I(!0), u = I(!0), d = I(!1), f = I(""), p = null, m = null, h;
1729
1741
  function _(e) {
1730
1742
  return {
@@ -1794,9 +1806,9 @@ function ar(e = "latest", t = {}) {
1794
1806
  }
1795
1807
  //#endregion
1796
1808
  //#region src/composables/useMemiBoardAuthorMemo.ts
1797
- var or = 100, sr = /* @__PURE__ */ new Map();
1809
+ var cr = 100, lr = /* @__PURE__ */ new Map();
1798
1810
  function $(e) {
1799
- let t = sr.get(e);
1811
+ let t = lr.get(e);
1800
1812
  return t || (t = he({
1801
1813
  text: "",
1802
1814
  sentiment: null,
@@ -1804,10 +1816,10 @@ function $(e) {
1804
1816
  pending: !1,
1805
1817
  saving: !1,
1806
1818
  error: ""
1807
- }), sr.set(e, t)), t;
1819
+ }), lr.set(e, t)), t;
1808
1820
  }
1809
- function cr(e) {
1810
- let t = Z(), n = () => M(), { user: r } = Vt(), { t: i, formatNumber: a } = j(), o = P(() => L(e)), s = P(() => `${r.value?.uid ?? ""}:${o.value}`), c = P(() => {
1821
+ function ur(e) {
1822
+ let t = Z(), n = () => M(), { user: r } = Ut(), { t: i, formatNumber: a } = j(), o = P(() => L(e)), s = P(() => `${r.value?.uid ?? ""}:${o.value}`), c = P(() => {
1811
1823
  let e = r.value?.uid, i = o.value;
1812
1824
  return !e || !i ? null : f(t, n(), e, i);
1813
1825
  }), l = P({
@@ -1876,7 +1888,7 @@ function cr(e) {
1876
1888
  }
1877
1889
  //#endregion
1878
1890
  //#region src/utils/subscribeCommentRange.ts
1879
- function lr(e, t, n, r, i, a) {
1891
+ function dr(e, t, n, r, i, a) {
1880
1892
  return W(K(g(e, t), X("postId", "==", n), ...r.rootId ? [X("rootId", "==", r.rootId), X("isReply", "==", !0)] : [X("parentId", "==", null)], G("createdAt", "asc"), G(B(), "asc"), ...r.hasMore && r.cursor ? [be(r.cursor)] : []), (e) => i(e.docs.map((e) => ({
1881
1893
  ...e.data(),
1882
1894
  id: e.id
@@ -1884,15 +1896,15 @@ function lr(e, t, n, r, i, a) {
1884
1896
  }
1885
1897
  //#endregion
1886
1898
  //#region src/composables/useMemiBoardComments.ts
1887
- var ur = 10, dr = 5, fr = 1e3;
1888
- function pr(e, { t, formatNumber: n }) {
1899
+ var fr = 10, pr = 5, mr = 1e3;
1900
+ function hr(e, { t, formatNumber: n }) {
1889
1901
  let r = e.trim();
1890
1902
  if (!r) throw Error(t("comments.bodyRequired"));
1891
- if (r.length > 1e3) throw Error(t("comments.bodyTooLong", { max: n(fr) }));
1903
+ if (r.length > 1e3) throw Error(t("comments.bodyTooLong", { max: n(mr) }));
1892
1904
  return r;
1893
1905
  }
1894
- function mr(e, t, n = {}) {
1895
- let r = Z(), i = Me(), a = () => M(), { isBoardHidden: o } = Gn(), s = j(), { t: c } = s, l = () => g(r, a()), u = P(() => L(e)), f = P(() => L(t)), p = () => !o(u.value), m = I([]), h = I(n.subscribe !== !1), _ = I([]), v = I(!1), b = I(n.subscribe !== !1), S = I(n.subscribe !== !1), C = null, w = 0, T = 0, E;
1906
+ function gr(e, t, n = {}) {
1907
+ let r = Z(), i = Me(), a = () => M(), { isBoardHidden: o } = qn(), s = j(), { t: c } = s, l = () => g(r, a()), u = P(() => L(e)), f = P(() => L(t)), p = () => !o(u.value), m = I([]), h = I(n.subscribe !== !1), _ = I([]), v = I(!1), b = I(n.subscribe !== !1), S = I(n.subscribe !== !1), C = null, w = 0, T = 0, E;
1896
1908
  function D(e) {
1897
1909
  return e.createdAt?.toMillis?.() ?? 0;
1898
1910
  }
@@ -1923,7 +1935,7 @@ function mr(e, t, n = {}) {
1923
1935
  function O() {
1924
1936
  E?.();
1925
1937
  let e = ++T;
1926
- E = lr(r, a(), f.value, {
1938
+ E = dr(r, a(), f.value, {
1927
1939
  cursor: C,
1928
1940
  hasMore: b.value
1929
1941
  }, (t) => {
@@ -1946,11 +1958,11 @@ function mr(e, t, n = {}) {
1946
1958
  U(11)
1947
1959
  ], n = await H(K(l(), ...t));
1948
1960
  if (e !== w) return;
1949
- let r = n.docs.slice(0, ur), i = r.map((e) => ({
1961
+ let r = n.docs.slice(0, fr), i = r.map((e) => ({
1950
1962
  id: e.id,
1951
1963
  ...e.data()
1952
1964
  })), a = new Set(_.value.map((e) => e.id));
1953
- _.value.push(...i.filter((e) => !a.has(e.id))), C = r.at(-1) ?? C, b.value = n.size > ur, O();
1965
+ _.value.push(...i.filter((e) => !a.has(e.id))), C = r.at(-1) ?? C, b.value = n.size > fr, O();
1954
1966
  } finally {
1955
1967
  e === w && (v.value = !1);
1956
1968
  }
@@ -1976,7 +1988,7 @@ function mr(e, t, n = {}) {
1976
1988
  postId: f.value,
1977
1989
  boardId: u.value,
1978
1990
  listed: p(),
1979
- body: pr(e.body, s),
1991
+ body: hr(e.body, s),
1980
1992
  authorUid: e.authorUid,
1981
1993
  authorName: e.authorName,
1982
1994
  authorPhoto: e.authorPhoto,
@@ -2024,13 +2036,13 @@ function mr(e, t, n = {}) {
2024
2036
  if (!e.id) return;
2025
2037
  if (e.parentId == null && (e.replyCount ?? 0) > 0) throw Error(c("comments.hasRepliesCannotDelete"));
2026
2038
  let t = ue(e.images, `${x(a(), f.value)}/comment-images/${e.id}/`);
2027
- await rn(r, a(), e.id);
2039
+ await on(r, a(), e.id);
2028
2040
  let n = Oe(r);
2029
2041
  n.delete(d(r, a(), e.id)), e.parentId && e.rootId && n.update(d(r, a(), e.rootId), { replyCount: we(-1) }), n.update(y(r, a(), f.value), { commentCount: we(-1) }), await n.commit(), await ae(t), _.value = _.value.filter((t) => t.id !== e.id), m.value = m.value.filter((t) => t.id !== e.id);
2030
2042
  }
2031
2043
  async function ce(e, t, n) {
2032
2044
  let i = {
2033
- body: pr(t, s),
2045
+ body: hr(t, s),
2034
2046
  updatedAt: q()
2035
2047
  };
2036
2048
  if (n !== void 0) {
@@ -2060,7 +2072,7 @@ function mr(e, t, n = {}) {
2060
2072
  deleteComment: se
2061
2073
  };
2062
2074
  }
2063
- function hr(e, t, n) {
2075
+ function _r(e, t, n) {
2064
2076
  let r = Z(), i = () => M(), a = () => g(r, i()), o = t.trim(), s = I([]), c = I(!1), l = I(!1), u = I(!0), d = null, f, p = 0, m = 0;
2065
2077
  function h(e) {
2066
2078
  return e.sort((e, t) => (e.createdAt?.toMillis?.() ?? 0) - (t.createdAt?.toMillis?.() ?? 0) || String(e.id).localeCompare(String(t.id)));
@@ -2068,7 +2080,7 @@ function hr(e, t, n) {
2068
2080
  function _() {
2069
2081
  f?.();
2070
2082
  let e = ++m;
2071
- f = lr(r, i(), o, {
2083
+ f = dr(r, i(), o, {
2072
2084
  rootId: n,
2073
2085
  cursor: d,
2074
2086
  hasMore: u.value
@@ -2093,11 +2105,11 @@ function hr(e, t, n) {
2093
2105
  U(6)
2094
2106
  ], r = await H(K(a(), ...t));
2095
2107
  if (e !== p) return;
2096
- let i = new Set(s.value.map((e) => e.id)), c = r.docs.slice(0, dr), f = c.map((e) => ({
2108
+ let i = new Set(s.value.map((e) => e.id)), c = r.docs.slice(0, pr), f = c.map((e) => ({
2097
2109
  id: e.id,
2098
2110
  ...e.data()
2099
2111
  })).filter((e) => !i.has(e.id));
2100
- s.value = h([...s.value, ...f]), d = c.at(-1) ?? d, l.value = !0, u.value = r.size > dr, _();
2112
+ s.value = h([...s.value, ...f]), d = c.at(-1) ?? d, l.value = !0, u.value = r.size > pr, _();
2101
2113
  } finally {
2102
2114
  e === p && (c.value = !1);
2103
2115
  }
@@ -2121,7 +2133,7 @@ function hr(e, t, n) {
2121
2133
  }
2122
2134
  //#endregion
2123
2135
  //#region src/composables/useMemiBoardLikes.ts
2124
- function gr(e, t) {
2136
+ function vr(e, t) {
2125
2137
  let n = () => M(), r = Z(), i = Ae(), { t: a } = j(), o = I(!1), s = I(!1), c;
2126
2138
  function l(e) {
2127
2139
  return me(r, n(), t, e);
@@ -2160,8 +2172,8 @@ function gr(e, t) {
2160
2172
  toggleLike: u
2161
2173
  };
2162
2174
  }
2163
- var _r = Symbol.for("memi-board:comment-likes");
2164
- function vr(e, t) {
2175
+ var yr = Symbol.for("memi-board:comment-likes");
2176
+ function br(e, t) {
2165
2177
  let n = () => M(), r = Z(), i = Ae(), { t: a } = j(), o = P(() => L(e)), s = P(() => L(t)), c = I(/* @__PURE__ */ new Set()), l = I(null), u;
2166
2178
  R([i, s], ([e, t]) => {
2167
2179
  u?.(), u = void 0, c.value = /* @__PURE__ */ new Set(), e && t && (u = W(K(b(r, n()), X("uid", "==", e.uid), X("postId", "==", t), X("target", "==", "comment")), (e) => {
@@ -2214,7 +2226,7 @@ function vr(e, t) {
2214
2226
  }
2215
2227
  //#endregion
2216
2228
  //#region src/composables/useMemiBoardReports.ts
2217
- var yr = 200, br = [
2229
+ var xr = 200, Sr = [
2218
2230
  {
2219
2231
  value: "spam",
2220
2232
  label: "스팸·광고"
@@ -2236,10 +2248,10 @@ var yr = 200, br = [
2236
2248
  label: "기타"
2237
2249
  }
2238
2250
  ];
2239
- function xr(e, t) {
2240
- return br.some((t) => t.value === e) ? A(t, `reports.reason.${e}`) : e || A(t, "reports.fallbackLabel");
2251
+ function Cr(e, t) {
2252
+ return Sr.some((t) => t.value === e) ? A(t, `reports.reason.${e}`) : e || A(t, "reports.fallbackLabel");
2241
2253
  }
2242
- function Sr(e, t) {
2254
+ function wr(e, t) {
2243
2255
  let n = () => M(), r = Z(), i = Ae(), { t: a } = j(), o = I(!1), s = I(!1), c;
2244
2256
  R(i, (e) => {
2245
2257
  if (c?.(), c = void 0, !e) {
@@ -2257,7 +2269,7 @@ function Sr(e, t) {
2257
2269
  if (!l) throw Error(a("reports.signInRequired"));
2258
2270
  if (s.value) return;
2259
2271
  if (c.authorUid && c.authorUid === l) throw Error(a("reports.cannotReportOwn"));
2260
- let d = br.some((e) => e.value === c.reason) ? c.reason : null;
2272
+ let d = Sr.some((e) => e.value === c.reason) ? c.reason : null;
2261
2273
  if (!d) throw Error(a("reports.reasonRequired"));
2262
2274
  let f = (c.detail || "").trim();
2263
2275
  if (f.length > 200) throw Error(a("reports.detailTooLong", { max: 200 }));
@@ -2286,9 +2298,9 @@ function Sr(e, t) {
2286
2298
  submitReport: l
2287
2299
  };
2288
2300
  }
2289
- var Cr = 20;
2290
- function wr(e = {}) {
2291
- let t = () => M(), n = Z(), { isAdmin: r, canManageContent: i, canManageBoard: a } = Vt(), o = e.pageSize ?? Cr, s = P(() => L(e.boardId)?.trim() || ""), c = I([]), d = I(!0), f = I(!1), p = I(!1), m = I(""), h = null, g;
2301
+ var Tr = 20;
2302
+ function Er(e = {}) {
2303
+ let t = () => M(), n = Z(), { isAdmin: r, canManageContent: i, canManageBoard: a } = Ut(), o = e.pageSize ?? Tr, s = P(() => L(e.boardId)?.trim() || ""), c = I([]), d = I(!0), f = I(!1), p = I(!1), m = I(""), h = null, g;
2292
2304
  function _(e) {
2293
2305
  return {
2294
2306
  id: e.id,
@@ -2342,7 +2354,7 @@ function wr(e = {}) {
2342
2354
  }
2343
2355
  //#endregion
2344
2356
  //#region src/composables/useMemiBoardViews.ts
2345
- function Tr() {
2357
+ function Dr() {
2346
2358
  let e = () => M(), t = Z();
2347
2359
  function n(t, n) {
2348
2360
  let { postsCollection: r } = e();
@@ -2382,20 +2394,20 @@ function Tr() {
2382
2394
  }
2383
2395
  //#endregion
2384
2396
  //#region src/composables/useMemiBoardModeration.ts
2385
- var Er = {
2397
+ var Or = {
2386
2398
  flagged: !1,
2387
2399
  category: "none",
2388
2400
  reason: "",
2389
2401
  via: "empty"
2390
2402
  };
2391
- function Dr(e = {}) {
2392
- let t = j(), n = P(() => N(L(e.locale)) ?? t.locale.value), r = v().moderation ?? {}, i = Me(), a = (e) => A(n.value, e), { isWriteRestricted: o, restrictedMessage: s, recordContentModerationBlock: c } = Vt(), l = An([...kn, ...r.localBlocklist ?? []]);
2403
+ function kr(e = {}) {
2404
+ let t = j(), n = P(() => N(L(e.locale)) ?? t.locale.value), r = v().moderation ?? {}, i = Me(), a = (e) => A(n.value, e), { isWriteRestricted: o, restrictedMessage: s, recordContentModerationBlock: c } = Ut(), l = Mn([...jn, ...r.localBlocklist ?? []]);
2393
2405
  function u(e) {
2394
2406
  let t = e.match(l);
2395
2407
  return t ? {
2396
2408
  flagged: !0,
2397
2409
  category: "abuse",
2398
- reason: On({
2410
+ reason: An({
2399
2411
  via: "local",
2400
2412
  localHit: t[0] || "",
2401
2413
  category: "abuse"
@@ -2410,7 +2422,7 @@ function Dr(e = {}) {
2410
2422
  });
2411
2423
  return Ze(a, {
2412
2424
  model: e,
2413
- systemInstruction: Cn(n.value),
2425
+ systemInstruction: Tn(n.value),
2414
2426
  generationConfig: {
2415
2427
  temperature: 0,
2416
2428
  maxOutputTokens: 256,
@@ -2439,7 +2451,7 @@ function Dr(e = {}) {
2439
2451
  };
2440
2452
  }
2441
2453
  function p(e, t = "") {
2442
- let n = On({
2454
+ let n = An({
2443
2455
  reason: e.reason,
2444
2456
  category: e.category,
2445
2457
  via: e.via
@@ -2467,26 +2479,26 @@ function Dr(e = {}) {
2467
2479
  };
2468
2480
  let i = e.trim();
2469
2481
  if (!i) return {
2470
- ...Er,
2482
+ ...Or,
2471
2483
  via: "empty"
2472
2484
  };
2473
2485
  if (t?.skipFilter) return {
2474
- ...Er,
2486
+ ...Or,
2475
2487
  via: "admin-override"
2476
2488
  };
2477
2489
  let c = u(i);
2478
2490
  if (c) return m(c);
2479
2491
  if (r.enabled === !1) return {
2480
- ...Er,
2492
+ ...Or,
2481
2493
  via: "disabled"
2482
2494
  };
2483
2495
  try {
2484
- let e = Mn((await d().generateContent(wn(i, n.value))).response.text());
2496
+ let e = Pn((await d().generateContent(En(i, n.value))).response.text());
2485
2497
  if (!e) return f();
2486
2498
  let t = {
2487
2499
  flagged: e.flagged,
2488
2500
  category: e.category,
2489
- reason: On({
2501
+ reason: An({
2490
2502
  reason: e.reason,
2491
2503
  category: e.category,
2492
2504
  via: "ai"
@@ -2506,22 +2518,22 @@ function Dr(e = {}) {
2506
2518
  via: "restricted"
2507
2519
  };
2508
2520
  if (r.enabled === !1 || !r.moderateImages) return {
2509
- ...Er,
2521
+ ...Or,
2510
2522
  via: "disabled"
2511
2523
  };
2512
2524
  try {
2513
2525
  let t = await new Promise((t, n) => {
2514
2526
  let r = new FileReader();
2515
2527
  r.onload = () => t(r.result.split(",")[1] ?? ""), r.onerror = n, r.readAsDataURL(e);
2516
- }), r = Mn((await d().generateContent([{ inlineData: {
2528
+ }), r = Pn((await d().generateContent([{ inlineData: {
2517
2529
  mimeType: e.type || "image/jpeg",
2518
2530
  data: t
2519
- } }, Tn(n.value)])).response.text());
2531
+ } }, Dn(n.value)])).response.text());
2520
2532
  if (!r) return f();
2521
2533
  let i = {
2522
2534
  flagged: r.flagged,
2523
2535
  category: r.category,
2524
- reason: On({
2536
+ reason: An({
2525
2537
  reason: r.reason,
2526
2538
  category: r.category,
2527
2539
  via: "ai"
@@ -2540,7 +2552,7 @@ function Dr(e = {}) {
2540
2552
  }
2541
2553
  //#endregion
2542
2554
  //#region src/composables/useMemiBoardUsers.ts
2543
- var Or = [
2555
+ var Ar = [
2544
2556
  "owner",
2545
2557
  "admin",
2546
2558
  "manager",
@@ -2551,10 +2563,10 @@ var Or = [
2551
2563
  label: A("ko", `common.role.${e}`),
2552
2564
  description: A("ko", `boardUsers.roleDescription.${e}`)
2553
2565
  }));
2554
- function kr(e = {}) {
2566
+ function jr(e = {}) {
2555
2567
  let t = () => M(), n = Z(), { t: r } = j(), i = P(() => L(e.enabled) ?? !0 ? c(n, t()) : null), a = ke(i, { ssrKey: p(t(), "users-management") });
2556
2568
  async function o(e, i) {
2557
- if (!Or.some((e) => e.value === i)) throw Error(r("boardUsers.unsupportedRole"));
2569
+ if (!Ar.some((e) => e.value === i)) throw Error(r("boardUsers.unsupportedRole"));
2558
2570
  await Y(h(n, t(), e), {
2559
2571
  role: i,
2560
2572
  updatedAt: q()
@@ -2568,7 +2580,7 @@ function kr(e = {}) {
2568
2580
  }
2569
2581
  //#endregion
2570
2582
  //#region src/composables/useMemiBoardUserProfile.ts
2571
- function Ar(e) {
2583
+ function Mr(e) {
2572
2584
  let t = Z(), n = () => M(), r = P(() => L(e)), i = P(() => {
2573
2585
  let e = r.value;
2574
2586
  return e ? h(t, n(), e) : null;
@@ -2580,7 +2592,7 @@ function Ar(e) {
2580
2592
  }
2581
2593
  //#endregion
2582
2594
  //#region src/utils/section.ts
2583
- var jr = [
2595
+ var Nr = [
2584
2596
  3,
2585
2597
  4,
2586
2598
  5,
@@ -2590,7 +2602,7 @@ var jr = [
2590
2602
  9,
2591
2603
  12
2592
2604
  ];
2593
- function Mr(e) {
2605
+ function Pr(e) {
2594
2606
  return [
2595
2607
  {
2596
2608
  value: "list",
@@ -2606,7 +2618,7 @@ function Mr(e) {
2606
2618
  }
2607
2619
  ];
2608
2620
  }
2609
- function Nr(e) {
2621
+ function Fr(e) {
2610
2622
  return [{
2611
2623
  value: "latest",
2612
2624
  label: A(e, "sectionUtil.sort.latest")
@@ -2615,18 +2627,18 @@ function Nr(e) {
2615
2627
  label: A(e, "sectionUtil.sort.likes")
2616
2628
  }];
2617
2629
  }
2618
- var Pr = Mr("ko"), Fr = Nr("ko");
2619
- function Ir(e) {
2630
+ var Ir = Pr("ko"), Lr = Fr("ko");
2631
+ function Rr(e) {
2620
2632
  return e === "list" ? 5 : 4;
2621
2633
  }
2622
- function Lr(e, t) {
2634
+ function zr(e, t) {
2623
2635
  let n = Math.floor(Number(t));
2624
- return Number.isFinite(n) ? Math.min(e === "list" ? 20 : 10, Math.max(1, n)) : Ir(e);
2636
+ return Number.isFinite(n) ? Math.min(e === "list" ? 20 : 10, Math.max(1, n)) : Rr(e);
2625
2637
  }
2626
- function Rr(e) {
2638
+ function Br(e) {
2627
2639
  return e === 12 ? null : 12 - e;
2628
2640
  }
2629
- function zr(e) {
2641
+ function Vr(e) {
2630
2642
  return {
2631
2643
  3: "col-span-12 md:col-span-3",
2632
2644
  4: "col-span-12 md:col-span-4",
@@ -2638,36 +2650,36 @@ function zr(e) {
2638
2650
  12: "col-span-12"
2639
2651
  }[e];
2640
2652
  }
2641
- function Br(e) {
2653
+ function Hr(e) {
2642
2654
  return e === "sm" ? "min-h-40" : e === "lg" ? "min-h-80" : "min-h-56";
2643
2655
  }
2644
- function Vr(e) {
2656
+ function Ur(e) {
2645
2657
  let t = e.boardId || e.category || "free";
2646
2658
  return e.slug ? `/board/${t}/${e.slug}` : `/board/${t}`;
2647
2659
  }
2648
- function Hr(e) {
2660
+ function Wr(e) {
2649
2661
  return e.kind === "comments" ? e.sort === "likes" ? "/boardComments?sort=likes" : "/boardComments" : e.kind === "list" && e.boardId ? `/board/${e.boardId}` : "/board";
2650
2662
  }
2651
- function Ur(e) {
2663
+ function Gr(e) {
2652
2664
  if (e.boardId && e.postId) return `/board/${e.boardId}/post/${e.postId}`;
2653
2665
  }
2654
- function Wr(e) {
2666
+ function Kr(e) {
2655
2667
  return e.previewImage || e.attachments?.find((e) => e.type.startsWith("image/"))?.url;
2656
2668
  }
2657
- function Gr(e, t) {
2658
- return gt(e, t).trim() || _t(e, t).trim() || A(t, "sectionUtil.untitledPost");
2669
+ function qr(e, t) {
2670
+ return vt(e, t).trim() || yt(e, t).trim() || A(t, "sectionUtil.untitledPost");
2659
2671
  }
2660
- function Kr(e) {
2672
+ function Jr(e) {
2661
2673
  return e === "image" || e === "video" ? "media" : "list";
2662
2674
  }
2663
- function qr(e, t) {
2664
- return A(t, Kr(e) === "media" ? "sectionUtil.view.media" : "sectionUtil.view.list");
2675
+ function Yr(e, t) {
2676
+ return A(t, Jr(e) === "media" ? "sectionUtil.view.media" : "sectionUtil.view.list");
2665
2677
  }
2666
- function Jr(e, t) {
2678
+ function Xr(e, t) {
2667
2679
  return e ? t(e)?.listView ?? "default" : "default";
2668
2680
  }
2669
- function Yr(e, t) {
2670
- let n = e.map((e) => Jr(e.boardId, t));
2681
+ function Zr(e, t) {
2682
+ let n = e.map((e) => Xr(e.boardId, t));
2671
2683
  if (!n.length) return "default";
2672
2684
  if (n.every((e) => e === n[0])) return n[0];
2673
2685
  let r = n.filter((e) => e === "image").length, i = n.filter((e) => e === "video").length;
@@ -2675,27 +2687,27 @@ function Yr(e, t) {
2675
2687
  }
2676
2688
  //#endregion
2677
2689
  //#region src/composables/useMemiBoardSections.ts
2678
- var Xr = [];
2679
- function Zr(e) {
2690
+ var Qr = [];
2691
+ function $r(e) {
2680
2692
  let t = Number(e.cols);
2681
- if (jr.includes(t)) return t;
2693
+ if (Nr.includes(t)) return t;
2682
2694
  let n = e.width;
2683
2695
  return n === "xs" ? 3 : n === "sm" ? 4 : n === "lg" || Number(e.span) === 2 ? 12 : 6;
2684
2696
  }
2685
- function Qr(e) {
2697
+ function ei(e) {
2686
2698
  let t = e.height ?? e.size;
2687
2699
  return t === "sm" || t === "lg" ? t : "md";
2688
2700
  }
2689
- function $r(e) {
2701
+ function ti(e) {
2690
2702
  return e.kind === "post" || e.kind === "comments" ? e.kind : "list";
2691
2703
  }
2692
- function ei(e) {
2704
+ function ni(e) {
2693
2705
  return e.sort === "likes" ? "likes" : "latest";
2694
2706
  }
2695
- function ti(e) {
2707
+ function ri(e) {
2696
2708
  return (typeof e.boardId == "string" ? e.boardId.trim() : "") || null;
2697
2709
  }
2698
- function ni(e, t) {
2710
+ function ii(e, t) {
2699
2711
  let n = Array.isArray(e.postIds) ? e.postIds : [], r = [];
2700
2712
  for (let e of n) {
2701
2713
  let n = String(e || "").trim();
@@ -2703,34 +2715,34 @@ function ni(e, t) {
2703
2715
  }
2704
2716
  return r;
2705
2717
  }
2706
- function ri(e, t) {
2718
+ function ai(e, t) {
2707
2719
  return A(t, "sections.defaultTitle", { n: e + 1 });
2708
2720
  }
2709
- function ii(e, t, n) {
2710
- let r = $r(e), i = Lr(r, e.count), a = Q(e.i18n), o = ut(e.i18nSource);
2721
+ function oi(e, t, n) {
2722
+ let r = ti(e), i = zr(r, e.count), a = Q(e.i18n), o = ut(e.i18nSource);
2711
2723
  return {
2712
2724
  id: String(e.id || `box-${t + 1}`),
2713
- title: String(e.title || ri(t, n)),
2725
+ title: String(e.title || ai(t, n)),
2714
2726
  showTitle: e.showTitle !== !1,
2715
2727
  kind: r,
2716
- boardId: ti(e),
2728
+ boardId: ri(e),
2717
2729
  count: i,
2718
- postIds: ni(e, i),
2719
- sort: ei(e),
2720
- cols: Zr(e),
2721
- height: Qr(e),
2730
+ postIds: ii(e, i),
2731
+ sort: ni(e),
2732
+ cols: $r(e),
2733
+ height: ei(e),
2722
2734
  order: t,
2723
2735
  ...a ? { i18n: a } : {},
2724
2736
  ...o ? { i18nSource: o } : {}
2725
2737
  };
2726
2738
  }
2727
- function ai() {
2728
- let { locale: e } = j(), { translateEntries: t } = Bn(), r = Z(), i = z(r, "settings", "homeLayout"), { data: a, pending: o } = je(i), s = P(() => (Array.isArray(a.value?.blocks) ? a.value.blocks : Xr).map((t, n) => ii(t, n, e.value)));
2739
+ function si() {
2740
+ let { locale: e } = j(), { translateEntries: t } = Hn(), r = Z(), i = z(r, "settings", "homeLayout"), { data: a, pending: o } = je(i), s = P(() => (Array.isArray(a.value?.blocks) ? a.value.blocks : Qr).map((t, n) => oi(t, n, e.value)));
2729
2741
  async function c(r) {
2730
2742
  if (!n()) return r;
2731
2743
  let i = e.value, a = r.filter((e) => {
2732
2744
  let t = e.title.trim();
2733
- return t ? !(e.i18nSource?.title === t && pt(e.i18n, ["title"])) : !1;
2745
+ return t ? !(e.i18nSource?.title === t && ht(e.i18n, ["title"])) : !1;
2734
2746
  });
2735
2747
  if (!a.length) return r;
2736
2748
  let o = await t(a.map((e) => ({
@@ -2745,7 +2757,7 @@ function ai() {
2745
2757
  let t = o[e.id];
2746
2758
  if (!t) return e;
2747
2759
  let n = e.title.trim(), r = ft(i, { title: n }, t);
2748
- return pt(r, ["title"]) ? {
2760
+ return ht(r, ["title"]) ? {
2749
2761
  ...e,
2750
2762
  title: n,
2751
2763
  i18n: r,
@@ -2757,18 +2769,18 @@ function ai() {
2757
2769
  });
2758
2770
  }
2759
2771
  function l(t, n) {
2760
- let r = t, i = $r(r), a = Lr(i, t.count), o = t.title.trim() || ri(n, e.value);
2772
+ let r = t, i = ti(r), a = zr(i, t.count), o = t.title.trim() || ai(n, e.value);
2761
2773
  return {
2762
2774
  id: t.id,
2763
2775
  title: o,
2764
2776
  showTitle: t.showTitle !== !1,
2765
2777
  kind: i,
2766
- boardId: ti(r),
2778
+ boardId: ri(r),
2767
2779
  count: a,
2768
- postIds: ni(r, a),
2769
- sort: ei(r),
2770
- cols: Zr(r),
2771
- height: Qr(r),
2780
+ postIds: ii(r, a),
2781
+ sort: ni(r),
2782
+ cols: $r(r),
2783
+ height: ei(r),
2772
2784
  order: n,
2773
2785
  ...t.i18n ? { i18n: t.i18n } : {},
2774
2786
  ...t.i18nSource?.title === o ? { i18nSource: t.i18nSource } : {}
@@ -2799,7 +2811,7 @@ function ai() {
2799
2811
  }
2800
2812
  //#endregion
2801
2813
  //#region src/composables/useMemiBoardMiniPosts.ts
2802
- function oi(e, t) {
2814
+ function ci(e, t) {
2803
2815
  let n = Z(), r = () => M(), i = L(e)?.trim() || "all", a = P(() => {
2804
2816
  let i = Math.max(1, Math.floor(Number(L(t)) || 5)), a = L(e)?.trim() || "", s = [X("isPublished", "==", !0), X("listed", "==", !0)];
2805
2817
  return a && s.unshift(X("boardId", "==", a)), K(o(n, r()), ...s, G("createdAt", "desc"), U(i));
@@ -2810,7 +2822,7 @@ function oi(e, t) {
2810
2822
  error: l
2811
2823
  };
2812
2824
  }
2813
- function si(e) {
2825
+ function li(e) {
2814
2826
  let t = Z(), n = () => M(), r = P(() => [...new Set(L(e).map((e) => e.trim()).filter(Boolean))].slice(0, 10)), i = r.value.join(",") || "none", a = P(() => r.value.length ? K(o(t, n()), X(B(), "in", r.value)) : null), { data: s, pending: c, error: l } = ke(a, { ssrKey: p(n(), `mini-picked/${i}`) });
2815
2827
  return {
2816
2828
  posts: P(() => {
@@ -2827,22 +2839,42 @@ function si(e) {
2827
2839
  }
2828
2840
  //#endregion
2829
2841
  //#region src/data/versionHistory.ts
2830
- var ci = [
2842
+ var ui = [
2843
+ {
2844
+ version: "0.52.0",
2845
+ date: "2026-10-29",
2846
+ highlights: [
2847
+ "게시판마다 번역할 언어를 골라 원하는 언어로만 번역할 수 있어요",
2848
+ "전체 게시판 설정에서 번역 언어·자동 번역·게시판 언어를 모든 게시판에 한 번에 적용할 수 있어요",
2849
+ "번역이 안 된 게시판 이름·설명을 한 번에 번역할 수 있어요",
2850
+ "게시판 소개 문장이 10개 언어로 보이고, 버전 기록은 영어로도 볼 수 있어요"
2851
+ ],
2852
+ highlightsEn: [
2853
+ "Choose which languages each board is translated into",
2854
+ "Apply translation languages, auto-translation, and board language to all boards at once",
2855
+ "Translate the names and descriptions of untranslated boards in one go",
2856
+ "The board intro now appears in 10 languages, and the version history is available in English"
2857
+ ]
2858
+ },
2831
2859
  {
2832
2860
  version: "0.51.2",
2833
2861
  date: "2026-10-29",
2834
- highlights: ["목록 카드의 작성자 이름이 오른쪽에 맞춰져요", "작성자 메모는 작성자 메뉴에서 열 수 있어요"]
2862
+ highlights: ["목록 카드의 작성자 이름이 오른쪽에 맞춰져요", "작성자 메모는 작성자 메뉴에서 열 수 있어요"],
2863
+ highlightsEn: ["Author names on list cards are right-aligned", "Open author memos from the author menu"]
2835
2864
  },
2836
2865
  {
2837
2866
  version: "0.51.1",
2838
2867
  date: "2026-10-29",
2839
- highlights: ["글 수정 화면에서도 공지로 등록하거나 해제할 수 있어요"]
2868
+ highlights: ["글 수정 화면에서도 공지로 등록하거나 해제할 수 있어요"],
2869
+ highlightsEn: ["Pin or unpin a notice from the post edit screen too"]
2840
2870
  },
2841
2871
  {
2842
2872
  version: "0.51.0",
2843
2873
  date: "2026-10-29",
2844
2874
  highlights: ["게시판 매니저가 글을 공지로 지정하거나 해제할 수 있어요", "공지는 목록 맨 위에 따로 모여 보여요"],
2845
- note: "호스트 프로젝트의 Firestore 규칙과 인덱스도 함께 갱신해야 합니다."
2875
+ highlightsEn: ["Board managers can pin posts as notices or unpin them", "Notices are grouped at the top of the list"],
2876
+ note: "호스트 프로젝트의 Firestore 규칙과 인덱스도 함께 갱신해야 합니다.",
2877
+ noteEn: "Hosts must also update their Firestore rules and indexes."
2846
2878
  },
2847
2879
  {
2848
2880
  version: "0.50.0",
@@ -2852,17 +2884,25 @@ var ci = [
2852
2884
  "최근 업로드를 보호하고, 삭제 전에 연결과 관리자 권한을 다시 확인해요",
2853
2885
  "파일 목록과 비공개 댓글 조회의 보안 규칙을 강화했어요"
2854
2886
  ],
2855
- note: "호스트 프로젝트의 Firestore·Storage 규칙도 함께 갱신해야 합니다."
2887
+ highlightsEn: [
2888
+ "Find orphaned files, comments and likes in board settings and delete the ones you pick",
2889
+ "Recent uploads are protected, and links and admin rights are checked again before deleting",
2890
+ "Tightened security rules for file listing and private comment reads"
2891
+ ],
2892
+ note: "호스트 프로젝트의 Firestore·Storage 규칙도 함께 갱신해야 합니다.",
2893
+ noteEn: "Hosts must also update their Firestore and Storage rules."
2856
2894
  },
2857
2895
  {
2858
2896
  version: "0.49.0",
2859
2897
  date: "2026-10-29",
2860
- highlights: ["보드 설정에서 글 작성 시 자동 번역을 켤 수 있어요. 기본값은 꺼짐이에요", "새 글의 제목·요약·본문을 10개 언어로 번역해 함께 저장해요"]
2898
+ highlights: ["보드 설정에서 글 작성 시 자동 번역을 켤 수 있어요. 기본값은 꺼짐이에요", "새 글의 제목·요약·본문을 10개 언어로 번역해 함께 저장해요"],
2899
+ highlightsEn: ["Turn on automatic translation for new posts in board settings. It is off by default", "New posts save their title, summary and body in 10 languages"]
2861
2900
  },
2862
2901
  {
2863
2902
  version: "0.48.1",
2864
2903
  date: "2026-10-29",
2865
- highlights: ["댓글·답글·댓글 수정 입력창에 이미지를 붙여넣어 첨부할 수 있어요"]
2904
+ highlights: ["댓글·답글·댓글 수정 입력창에 이미지를 붙여넣어 첨부할 수 있어요"],
2905
+ highlightsEn: ["Paste images into the comment, reply and comment edit boxes to attach them"]
2866
2906
  },
2867
2907
  {
2868
2908
  version: "0.48.0",
@@ -2871,12 +2911,18 @@ var ci = [
2871
2911
  "댓글과 답글에 이미지를 한 장 첨부할 수 있어요",
2872
2912
  "큰 이미지는 올리기 전에 5MB 이하로 줄여요",
2873
2913
  "글을 지우면 댓글 이미지도 함께 지워요"
2914
+ ],
2915
+ highlightsEn: [
2916
+ "Attach one image to comments and replies",
2917
+ "Large images are shrunk to 5MB or less before upload",
2918
+ "Deleting a post also deletes its comment images"
2874
2919
  ]
2875
2920
  },
2876
2921
  {
2877
2922
  version: "0.47.1",
2878
2923
  date: "2026-10-29",
2879
- highlights: ["AI 글쓰기 도우미가 실패하면 그 시도는 하루 횟수에서 빠져요", "App Check 연결 안내를 한 번용 토큰과 SPA에 맞게 고쳤어요"]
2924
+ highlights: ["AI 글쓰기 도우미가 실패하면 그 시도는 하루 횟수에서 빠져요", "App Check 연결 안내를 한 번용 토큰과 SPA에 맞게 고쳤어요"],
2925
+ highlightsEn: ["Failed AI writing assistant attempts no longer count toward the daily limit", "Updated the App Check setup guide for one-time tokens and SPAs"]
2880
2926
  },
2881
2927
  {
2882
2928
  version: "0.47.0",
@@ -2885,12 +2931,18 @@ var ci = [
2885
2931
  "서비스에 따라 국제화를 끄고 한 언어로만 게시판을 사용할 수 있어요",
2886
2932
  "화면 언어 전환은 유지하면서 콘텐츠 번역만 끌 수도 있어요",
2887
2933
  "다른 사이트에 설치하는 방법과 번역 설정 안내를 정리했어요"
2934
+ ],
2935
+ highlightsEn: [
2936
+ "Turn off internationalization to run the board in a single language",
2937
+ "Or keep the language switcher and turn off only content translation",
2938
+ "Rewrote the guides for installing on other sites and setting up translation"
2888
2939
  ]
2889
2940
  },
2890
2941
  {
2891
2942
  version: "0.46.2",
2892
2943
  date: "2026-10-23",
2893
- highlights: ["게시판 표시 라벨 옆 버튼으로 이름과 설명을 10개 언어로 번역할 수 있어요", "저장할 때 자동으로 번역할지도 선택할 수 있어요"]
2944
+ highlights: ["게시판 표시 라벨 옆 버튼으로 이름과 설명을 10개 언어로 번역할 수 있어요", "저장할 때 자동으로 번역할지도 선택할 수 있어요"],
2945
+ highlightsEn: ["Translate board names and descriptions into 10 languages with the button next to the display label", "Choose whether to translate automatically on save"]
2894
2946
  },
2895
2947
  {
2896
2948
  version: "0.46.1",
@@ -2899,6 +2951,11 @@ var ci = [
2899
2951
  "게시판 이름과 설명을 저장하면 10개 언어로 번역해 둬요",
2900
2952
  "글쓰기 AI에서 한 번에 10개 언어로 번역해 저장할 수 있어요",
2901
2953
  "보는 언어로 제목과 본문이 보이고, 원문을 고치면 번역 대신 원문이 보여요"
2954
+ ],
2955
+ highlightsEn: [
2956
+ "Board names and descriptions are translated into 10 languages when saved",
2957
+ "The AI writing assistant can translate and save into 10 languages at once",
2958
+ "Titles and bodies show in the reader's language, and the original shows instead once it is edited"
2902
2959
  ]
2903
2960
  },
2904
2961
  {
@@ -2908,6 +2965,11 @@ var ci = [
2908
2965
  "일반 사용자·스태프·매니저·관리자·최고 관리자, 다섯 가지 역할을 지원해요",
2909
2966
  "게시판별 매니저가 설정과 스태프 배정을 관리할 수 있어요",
2910
2967
  "서비스에 맞게 역할 이름을 바꿀 수 있어요"
2968
+ ],
2969
+ highlightsEn: [
2970
+ "Five roles are supported: user, staff, manager, admin and owner",
2971
+ "Per-board managers can manage settings and staff assignments",
2972
+ "Rename roles to fit your service"
2911
2973
  ]
2912
2974
  },
2913
2975
  {
@@ -2917,93 +2979,120 @@ var ci = [
2917
2979
  "일반 계정 글쓰기·초안 조회와 태그 입력을 고쳤어요",
2918
2980
  "댓글·답글 수정과 좋아요가 다른 사용자에게 바로 반영돼요",
2919
2981
  "글 삭제·첨부파일 보호·게시판 공개 범위 변경을 보강했어요"
2982
+ ],
2983
+ highlightsEn: [
2984
+ "Fixed posting, draft viewing and tag input for regular accounts",
2985
+ "Comment and reply edits and likes show up for other users right away",
2986
+ "Strengthened post deletion, attachment protection and board visibility changes"
2920
2987
  ]
2921
2988
  },
2922
2989
  {
2923
2990
  version: "0.45.0",
2924
2991
  date: "2026-10-01",
2925
- highlights: ["게시판을 10개 언어로 볼 수 있어요 (한국어·영어·일본어·독일어·프랑스어·스페인어·포르투갈어·중국어·아랍어·인도네시아어)", "게시판 설정에서 게시판마다 언어를 고정하거나 사이트 언어를 따르게 할 수 있어요"]
2992
+ highlights: ["게시판을 10개 언어로 볼 수 있어요 (한국어·영어·일본어·독일어·프랑스어·스페인어·포르투갈어·중국어·아랍어·인도네시아어)", "게시판 설정에서 게시판마다 언어를 고정하거나 사이트 언어를 따르게 할 수 있어요"],
2993
+ highlightsEn: ["View the board in 10 languages (Korean, English, Japanese, German, French, Spanish, Portuguese, Chinese, Arabic and Indonesian)", "In board settings, fix each board's language or follow the site language"]
2926
2994
  },
2927
2995
  {
2928
2996
  version: "0.44.1",
2929
2997
  date: "2026-10-01",
2930
- highlights: ["Mac Safari에서 한글을 입력하고 바로 등록 버튼을 누를 때 두 번 눌러야 하던 문제를 고쳤어요"]
2998
+ highlights: ["Mac Safari에서 한글을 입력하고 바로 등록 버튼을 누를 때 두 번 눌러야 하던 문제를 고쳤어요"],
2999
+ highlightsEn: ["Fixed having to press submit twice after typing Korean in Mac Safari"]
2931
3000
  },
2932
3001
  {
2933
3002
  version: "0.44.0",
2934
3003
  date: "2026-10-01",
2935
- highlights: ["글을 저장하고 미리보기가 뜨기까지 기다리는 시간이 줄었어요 (특히 iPhone·Mac Safari)"]
3004
+ highlights: ["글을 저장하고 미리보기가 뜨기까지 기다리는 시간이 줄었어요 (특히 iPhone·Mac Safari)"],
3005
+ highlightsEn: ["Less waiting between saving a post and seeing the preview (especially on iPhone and Mac Safari)"]
2936
3006
  },
2937
3007
  {
2938
3008
  version: "0.42.0",
2939
3009
  date: "2026-08-27",
2940
- highlights: ["글쓰기에서 문단 옆 + 버튼이나 / 입력으로 제목·목록·이미지 같은 블록을 빠르게 넣을 수 있어요", "문단 메뉴에서 블록을 다른 형식으로 바꾸거나 복제·이동·삭제할 수 있어요"]
3010
+ highlights: ["글쓰기에서 문단 옆 + 버튼이나 / 입력으로 제목·목록·이미지 같은 블록을 빠르게 넣을 수 있어요", "문단 메뉴에서 블록을 다른 형식으로 바꾸거나 복제·이동·삭제할 수 있어요"],
3011
+ highlightsEn: ["Quickly insert blocks such as headings, lists and images with the + button next to a paragraph or by typing /", "Convert, duplicate, move or delete blocks from the paragraph menu"]
2941
3012
  },
2942
3013
  {
2943
3014
  version: "0.41.0",
2944
3015
  date: "2026-08-27",
2945
- highlights: ["AI 도우미 결과를 바로 적용하지 않고 이전/이후를 비교해보고 적용할 수 있어요"]
3016
+ highlights: ["AI 도우미 결과를 바로 적용하지 않고 이전/이후를 비교해보고 적용할 수 있어요"],
3017
+ highlightsEn: ["Compare before and after AI assistant results before applying them"]
2946
3018
  },
2947
3019
  {
2948
3020
  version: "0.40.0",
2949
3021
  date: "2026-08-27",
2950
- highlights: ["AI 글쓰기 도우미에서 원하는 문체를 직접 입력해 바꿀 수 있어요 (예: 경상도 사투리 스타일로)"]
3022
+ highlights: ["AI 글쓰기 도우미에서 원하는 문체를 직접 입력해 바꿀 수 있어요 (예: 경상도 사투리 스타일로)"],
3023
+ highlightsEn: ["Type any writing style you like into the AI writing assistant (e.g. in a Gyeongsang dialect style)"]
2951
3024
  },
2952
3025
  {
2953
3026
  version: "0.39.1",
2954
3027
  date: "2026-08-27",
2955
- highlights: ["관리자는 글쓰기 하단 스위치로 비속어 필터를 끄고 글을 올릴 수 있어요"]
3028
+ highlights: ["관리자는 글쓰기 하단 스위치로 비속어 필터를 끄고 글을 올릴 수 있어요"],
3029
+ highlightsEn: ["Admins can turn off the profanity filter with a switch at the bottom of the editor"]
2956
3030
  },
2957
3031
  {
2958
3032
  version: "0.39.0",
2959
3033
  date: "2026-08-21",
2960
- highlights: ["사진 글에서 원하는 사진을 대표로 고를 수 있어요", "목록 모양과 글쓰기 폼을 각각 선택할 수 있어요"]
3034
+ highlights: ["사진 글에서 원하는 사진을 대표로 고를 수 있어요", "목록 모양과 글쓰기 폼을 각각 선택할 수 있어요"],
3035
+ highlightsEn: ["Pick which photo represents a photo post", "Choose the list layout and the editor form separately"]
2961
3036
  },
2962
3037
  {
2963
3038
  version: "0.38.1",
2964
3039
  date: "2026-08-21",
2965
3040
  highlights: ["내가 쓴 글을 지울 때 첨부 사진 때문에 막히던 문제를 고쳤어요"],
2966
- note: "호스트는 Storage 예시 규칙의 memiBoardPosts/{postId}/{allPaths=**} 목록 블록을 넣고 배포해야 해요."
3041
+ highlightsEn: ["Fixed deleting your own post being blocked by attached photos"],
3042
+ note: "호스트는 Storage 예시 규칙의 memiBoardPosts/{postId}/{allPaths=**} 목록 블록을 넣고 배포해야 해요.",
3043
+ noteEn: "Hosts must add and deploy the memiBoardPosts/{postId}/{allPaths=**} list block from the example Storage rules."
2967
3044
  },
2968
3045
  {
2969
3046
  version: "0.38.0",
2970
3047
  date: "2026-08-21",
2971
3048
  highlights: ["숨김 게시판에는 댓글을 거의 쓰지 않아요. 필요하면 담당 스태프만 남길 수 있어요", "숨김 게시판 댓글은 공개 댓글 모음에 나오지 않아요"],
2972
- note: "호스트는 Firestore 규칙·댓글 listed 인덱스를 다시 배포해야 해요. 공개 보드는 한 번 저장하면 예전 댓글이 목록에 다시 채워져요."
3049
+ highlightsEn: ["Hidden boards rarely take comments. Only assigned staff can leave them when needed", "Comments on hidden boards don't appear in the public comment feed"],
3050
+ note: "호스트는 Firestore 규칙·댓글 listed 인덱스를 다시 배포해야 해요. 공개 보드는 한 번 저장하면 예전 댓글이 목록에 다시 채워져요.",
3051
+ noteEn: "Hosts must redeploy the Firestore rules and the comment listed index. Saving a public board once refills its older comments in the feed."
2973
3052
  },
2974
3053
  {
2975
3054
  version: "0.36.0",
2976
3055
  date: "2026-08-19",
2977
3056
  highlights: ["게시물을 신고할 수 있어요. 담당 스태프가 내용을 확인해요", "스태프는 지정된 게시판만 관리해요"],
2978
- note: "호스트는 Firestore 신고 규칙·인덱스를 배포해야 해요."
3057
+ highlightsEn: ["Report posts. Assigned staff review them", "Staff manage only the boards assigned to them"],
3058
+ note: "호스트는 Firestore 신고 규칙·인덱스를 배포해야 해요.",
3059
+ noteEn: "Hosts must deploy the Firestore report rules and indexes."
2979
3060
  },
2980
3061
  {
2981
3062
  version: "0.35.0",
2982
3063
  date: "2026-08-19",
2983
- highlights: ["메인에 게시목록·게시물·댓글모음 섹션을 올릴 수 있어요", "댓글모음은 최신순이나 좋아요순으로 네 개부터 보여요"]
3064
+ highlights: ["메인에 게시목록·게시물·댓글모음 섹션을 올릴 수 있어요", "댓글모음은 최신순이나 좋아요순으로 네 개부터 보여요"],
3065
+ highlightsEn: ["Put post list, post and comment feed sections on the main page", "The comment feed starts with four comments, newest or most liked first"]
2984
3066
  },
2985
3067
  {
2986
3068
  version: "0.34.0",
2987
3069
  date: "2026-08-19",
2988
3070
  highlights: ["댓글을 최신순이나 좋아요순으로 모아 볼 수 있어요"],
2989
- note: "호스트는 좋아요순 목록용 Firestore 인덱스(likeCount + createdAt)를 배포해야 해요."
3071
+ highlightsEn: ["Browse comments newest first or most liked first"],
3072
+ note: "호스트는 좋아요순 목록용 Firestore 인덱스(likeCount + createdAt)를 배포해야 해요.",
3073
+ noteEn: "Hosts must deploy the Firestore index for the most-liked list (likeCount + createdAt)."
2990
3074
  },
2991
3075
  {
2992
3076
  version: "0.33.0",
2993
3077
  date: "2026-08-19",
2994
3078
  highlights: ["댓글과 답글에도 좋아요를 누를 수 있어요", "글을 지우면 댓글 좋아요도 함께 삭제돼요"],
2995
- note: "호스트는 Firestore 규칙과 좋아요 인덱스를 병합·배포해야 댓글 좋아요가 동작해요."
3079
+ highlightsEn: ["Like comments and replies too", "Deleting a post also deletes its comment likes"],
3080
+ note: "호스트는 Firestore 규칙과 좋아요 인덱스를 병합·배포해야 댓글 좋아요가 동작해요.",
3081
+ noteEn: "Hosts must merge and deploy the Firestore rules and like indexes for comment likes to work."
2996
3082
  },
2997
3083
  {
2998
3084
  version: "0.32.0",
2999
3085
  date: "2026-08-14",
3000
- highlights: ["글쓰기에서 사진 파일을 올리거나 외부 이미지 주소를 넣을 수 있어요", "게시판을 지우면 그 안의 글·댓글·이미지도 함께 삭제돼요"]
3086
+ highlights: ["글쓰기에서 사진 파일을 올리거나 외부 이미지 주소를 넣을 수 있어요", "게시판을 지우면 그 안의 글·댓글·이미지도 함께 삭제돼요"],
3087
+ highlightsEn: ["Upload photo files or add external image URLs while writing", "Deleting a board also deletes its posts, comments and images"]
3001
3088
  },
3002
3089
  {
3003
3090
  version: "0.31.0",
3004
3091
  date: "2026-08-14",
3005
3092
  highlights: ["글쓰기 화면에 AI 도우미가 생겼어요. 맞춤법, 다듬기, 이어서 쓰기, 요약, 번역, 제목을 도와줘요", "AI 도우미는 하루 10번까지 사용할 수 있어요"],
3006
- note: "호스트는 Firestore 규칙에 AI 글쓰기 횟수 필드를 병합·배포해야 제한이 지켜져요."
3093
+ highlightsEn: ["An AI assistant joins the editor. It helps with spelling, polishing, continuing, summarizing, translating and titles", "Use the AI assistant up to 10 times a day"],
3094
+ note: "호스트는 Firestore 규칙에 AI 글쓰기 횟수 필드를 병합·배포해야 제한이 지켜져요.",
3095
+ noteEn: "Hosts must merge and deploy the AI writing count fields in their Firestore rules for the limit to apply."
3007
3096
  },
3008
3097
  {
3009
3098
  version: "0.30.2",
@@ -3012,12 +3101,18 @@ var ci = [
3012
3101
  "일반 목록 썸네일을 조금 작은 라운드 사각으로 바꿨어요",
3013
3102
  "글쓰기 도구 모음을 서식 메뉴로 줄여 좁은 화면에서도 잘리지 않아요",
3014
3103
  "다시 로그인해도 바꿔 둔 프로필 이름과 사진이 유지돼요"
3104
+ ],
3105
+ highlightsEn: [
3106
+ "List thumbnails are now slightly smaller rounded squares",
3107
+ "The editor toolbar folds into a format menu so it fits on narrow screens",
3108
+ "Your changed profile name and photo stay after signing in again"
3015
3109
  ]
3016
3110
  },
3017
3111
  {
3018
3112
  version: "0.30.1",
3019
3113
  date: "2026-08-13",
3020
- highlights: ["게시물 상단의 목록 버튼을 간결한 왼쪽 화살표로 바꿨어요", "이제 main 브랜치에 push하면 npm 배포가 자동으로 진행돼요"]
3114
+ highlights: ["게시물 상단의 목록 버튼을 간결한 왼쪽 화살표로 바꿨어요", "이제 main 브랜치에 push하면 npm 배포가 자동으로 진행돼요"],
3115
+ highlightsEn: ["The list button at the top of a post is now a simple left arrow", "Pushing to the main branch now publishes to npm automatically"]
3021
3116
  },
3022
3117
  {
3023
3118
  version: "0.30.0",
@@ -3027,13 +3122,21 @@ var ci = [
3027
3122
  "목록은 게시판 설정에서 정한 보기 방식을 사용하고, 테스트용 보기 전환 버튼은 숨겼어요",
3028
3123
  "글쓰기 중 사진 업로드 권한과 Storage 버킷 설정 안내를 보강했어요"
3029
3124
  ],
3030
- note: "이미지 업로드가 거부되면 호스트의 Storage 규칙과 firebase.json 버킷 설정을 최신 예시로 확인해 주세요."
3125
+ highlightsEn: [
3126
+ "Post with only attached photos and no body text",
3127
+ "Lists use the view chosen in board settings, and the test view switcher is hidden",
3128
+ "Improved guidance on photo upload permissions and Storage bucket setup"
3129
+ ],
3130
+ note: "이미지 업로드가 거부되면 호스트의 Storage 규칙과 firebase.json 버킷 설정을 최신 예시로 확인해 주세요.",
3131
+ noteEn: "If image uploads are rejected, check the host's Storage rules and firebase.json bucket settings against the latest example."
3031
3132
  },
3032
3133
  {
3033
3134
  version: "0.29.1",
3034
3135
  date: "2026-08-13",
3035
3136
  highlights: ["프로필 이름이나 사진을 바꾸면 예전에 작성한 글과 댓글의 작성자 정보도 함께 바뀌어요", "나를 대상으로 작성된 답글의 표시 이름도 새 프로필 이름으로 맞춰져요"],
3036
- note: "이 기능을 쓰려면 호스트가 Firestore 보안 규칙을 최신 예시로 갱신해야 해요."
3137
+ highlightsEn: ["Changing your profile name or photo also updates the author info on your earlier posts and comments", "Replies addressed to you show your new profile name too"],
3138
+ note: "이 기능을 쓰려면 호스트가 Firestore 보안 규칙을 최신 예시로 갱신해야 해요.",
3139
+ noteEn: "Hosts must update their Firestore security rules to the latest example to use this."
3037
3140
  },
3038
3141
  {
3039
3142
  version: "0.27.0",
@@ -3042,23 +3145,32 @@ var ci = [
3042
3145
  "일반 목록 디자인을 다듬었어요 — 사진이 있는 글은 제목과 작성자 사이에 동그란 미리보기로 보여요",
3043
3146
  "조밀 보기를 커뮤니티 게시판처럼 한 줄로 다시 짰어요 — 시간·작성자·조회·추천·게시판을 한 줄에, 사진 있으면 오른쪽에 작은 미리보기가 붙어요",
3044
3147
  "두 줄 이상으로 쓴 글은 목록 요약에서도 줄바꿈이 그대로 보여요"
3148
+ ],
3149
+ highlightsEn: [
3150
+ "Refined the default list. Posts with photos show a round preview between the title and author",
3151
+ "Rebuilt the dense view as a single line like a community board, with time, author, views, likes and board on one line and a small preview on the right when there is a photo",
3152
+ "Line breaks in multi-line posts are kept in list summaries"
3045
3153
  ]
3046
3154
  },
3047
3155
  {
3048
3156
  version: "0.26.0",
3049
3157
  date: "2026-08-12",
3050
- highlights: ["다른 사람 글을 보다가 그 사람에 대해 나만 보는 메모를 남길 수 있어요 — 좋음/나쁨 표시도 같이 남겨요", "메모는 작성자 이름 옆 아이콘을 눌러서 쓰고, 다른 사람은(관리자 포함) 절대 못 봐요"]
3158
+ highlights: ["다른 사람 글을 보다가 그 사람에 대해 나만 보는 메모를 남길 수 있어요 — 좋음/나쁨 표시도 같이 남겨요", "메모는 작성자 이름 옆 아이콘을 눌러서 쓰고, 다른 사람은(관리자 포함) 절대 못 봐요"],
3159
+ highlightsEn: ["Leave a private memo about another author while reading their post, with a good or bad mark", "Write memos from the icon next to the author name. Nobody else, not even admins, can see them"]
3051
3160
  },
3052
3161
  {
3053
3162
  version: "0.25.0",
3054
3163
  date: "2026-08-12",
3055
3164
  highlights: ["\"작성글 보기\"처럼 \"작성한 댓글 보기\"도 생겼어요 — 여러 게시판에 흩어진 내 댓글을 한 곳에서 모아 봐요"],
3056
- note: "이 기능을 쓰려면 호스트가 Firestore 보안 규칙을 최신 예시로 갱신해야 해요."
3165
+ highlightsEn: ["Like \"View posts\", there is now \"View comments\" to gather your comments from every board in one place"],
3166
+ note: "이 기능을 쓰려면 호스트가 Firestore 보안 규칙을 최신 예시로 갱신해야 해요.",
3167
+ noteEn: "Hosts must update their Firestore security rules to the latest example to use this."
3057
3168
  },
3058
3169
  {
3059
3170
  version: "0.24.0",
3060
3171
  date: "2026-08-11",
3061
- highlights: ["사진 올리는 코드를 분리해서, 게시판이 더 안정적으로 빌드되고 열려요"]
3172
+ highlights: ["사진 올리는 코드를 분리해서, 게시판이 더 안정적으로 빌드되고 열려요"],
3173
+ highlightsEn: ["Split out the photo upload code so the board builds and loads more reliably"]
3062
3174
  },
3063
3175
  {
3064
3176
  version: "0.23.0",
@@ -3069,42 +3181,56 @@ var ci = [
3069
3181
  "\"내가 좋아요한 글\" 같은 기능을 나중에 붙이기 쉬워졌어요",
3070
3182
  "날짜에 마우스를 올려야 보이던 작성 시각을 이제 눌러서 볼 수 있어요 — 폰에서도 똑같이 동작해요"
3071
3183
  ],
3072
- note: "기존 게시판 데이터와 호환되지 않아요 — 새로 설치하는 것처럼 다시 시작해야 해요."
3184
+ highlightsEn: [
3185
+ "Posts, comments and likes now live in separate memiBoardPosts, memiBoardComments and memiBoardLikes collections instead of under each board. The board ID is a field on each post",
3186
+ "Per-author post lists are lighter, found in one query instead of searching every board",
3187
+ "Features like \"posts I liked\" are easier to add later",
3188
+ "Tap a date to see the exact time instead of hovering, which also works on phones"
3189
+ ],
3190
+ note: "기존 게시판 데이터와 호환되지 않아요 — 새로 설치하는 것처럼 다시 시작해야 해요.",
3191
+ noteEn: "Not compatible with existing board data. Start over as if installing fresh."
3073
3192
  },
3074
3193
  {
3075
3194
  version: "0.21.1",
3076
3195
  date: "2026-08-07",
3077
- highlights: ["관리자가 쓴 댓글에 답글을 달 때 오류가 나던 문제를 고쳤어요"]
3196
+ highlights: ["관리자가 쓴 댓글에 답글을 달 때 오류가 나던 문제를 고쳤어요"],
3197
+ highlightsEn: ["Fixed an error when replying to a comment written by an admin"]
3078
3198
  },
3079
3199
  {
3080
3200
  version: "0.21.0",
3081
3201
  date: "2026-08-06",
3082
- highlights: ["카테고리 대신 게시판(보드)만 있어요 — 예전 카테고리 이름이 곧 보드 ID예요", "글은 memiBoards/{보드}/posts, 설정은 settings, 회원 역할은 memiBoardUsers 에 모아요"]
3202
+ highlights: ["카테고리 대신 게시판(보드)만 있어요 — 예전 카테고리 이름이 곧 보드 ID예요", "글은 memiBoards/{보드}/posts, 설정은 settings, 회원 역할은 memiBoardUsers 에 모아요"],
3203
+ highlightsEn: ["Boards replace categories. The old category name is now the board ID", "Posts live in memiBoards/{board}/posts, settings in settings, and member roles in memiBoardUsers"]
3083
3204
  },
3084
3205
  {
3085
3206
  version: "0.20.0",
3086
3207
  date: "2026-08-06",
3087
- highlights: ["게시판 데이터를 memiBoards/{게시판ID}/… 아래에 모았어요 (여러 게시판을 같은 프로젝트에 둘 수 있어요)", "설정·글·사용자 경로가 게시판마다 분리돼요 — 호스트는 boardId만 맞추면 돼요"]
3208
+ highlights: ["게시판 데이터를 memiBoards/{게시판ID}/… 아래에 모았어요 (여러 게시판을 같은 프로젝트에 둘 수 있어요)", "설정·글·사용자 경로가 게시판마다 분리돼요 — 호스트는 boardId만 맞추면 돼요"],
3209
+ highlightsEn: ["Board data now lives under memiBoards/{boardId}/… (so several boards can share one project)", "Settings, post and user paths are separated per board. Hosts only need to match the boardId"]
3088
3210
  },
3089
3211
  {
3090
3212
  version: "0.19.3",
3091
3213
  date: "2026-08-06",
3092
- highlights: ["이미지 목록 게시판은 인스타처럼 제목 없이 사진+내용만 올리면 돼요", "사진을 여러 장 올리고 끌어 순서를 바꿀 수 있어요 (첫 장이 대표)"]
3214
+ highlights: ["이미지 목록 게시판은 인스타처럼 제목 없이 사진+내용만 올리면 돼요", "사진을 여러 장 올리고 끌어 순서를 바꿀 수 있어요 (첫 장이 대표)"],
3215
+ highlightsEn: ["Image list boards work like Instagram, with just photos and text and no title", "Upload several photos and drag to reorder them (the first one is the cover)"]
3093
3216
  },
3094
3217
  {
3095
3218
  version: "0.19.2",
3096
3219
  date: "2026-08-06",
3097
- highlights: ["이미지만 넣고 글 없이 올리는 건 막았어요 — 본문에 글자를 넣어야 해요"]
3220
+ highlights: ["이미지만 넣고 글 없이 올리는 건 막았어요 — 본문에 글자를 넣어야 해요"],
3221
+ highlightsEn: ["Posting images without any text is blocked. The body needs some text"]
3098
3222
  },
3099
3223
  {
3100
3224
  version: "0.19.1",
3101
3225
  date: "2026-08-06",
3102
- highlights: ["공유 미리보기 이미지를 가벼운 썸네일로 쓰도록 바꿨어요 (카톡 등에 더 잘 맞아요)"]
3226
+ highlights: ["공유 미리보기 이미지를 가벼운 썸네일로 쓰도록 바꿨어요 (카톡 등에 더 잘 맞아요)"],
3227
+ highlightsEn: ["Share previews now use a lighter thumbnail image (better for messengers like KakaoTalk)"]
3103
3228
  },
3104
3229
  {
3105
3230
  version: "0.19.0",
3106
3231
  date: "2026-08-06",
3107
- highlights: ["게시판 목록·글 공유 미리보기(제목·설명·대표 이미지)를 모듈만으로 붙일 수 있어요", "호스트가 따로 SEO API를 만들 필요 없이, 공개 글 기준으로 미리보기가 채워져요"]
3232
+ highlights: ["게시판 목록·글 공유 미리보기(제목·설명·대표 이미지)를 모듈만으로 붙일 수 있어요", "호스트가 따로 SEO API를 만들 필요 없이, 공개 글 기준으로 미리보기가 채워져요"],
3233
+ highlightsEn: ["Add share previews (title, description and cover image) for board lists and posts with just the module", "Previews are filled from public posts without the host building its own SEO API"]
3108
3234
  },
3109
3235
  {
3110
3236
  version: "0.18.0",
@@ -3113,6 +3239,11 @@ var ci = [
3113
3239
  "게시판을 보임/숨김으로 나눌 수 있어요 — 숨김은 일기장처럼 권한이 있는 사람만 볼 수 있어요",
3114
3240
  "권한이 없는 글·게시판은 로딩에 멈추지 않고 안내 메시지로 알려 줘요",
3115
3241
  "글 붙여넣기로 넣은 이미지가 파일명과 함께 두 번 들어가던 문제를 고쳤어요"
3242
+ ],
3243
+ highlightsEn: [
3244
+ "Boards can be visible or hidden. Hidden boards are like a diary that only people with access can see",
3245
+ "Posts and boards you can't access show a message instead of loading forever",
3246
+ "Fixed pasted images being inserted twice along with their file name"
3116
3247
  ]
3117
3248
  },
3118
3249
  {
@@ -3122,6 +3253,11 @@ var ci = [
3122
3253
  "게시글 조회수가 생겼어요 — 로그인하지 않아도 조회가 쌓여요",
3123
3254
  "목록과 상세에서 조회수를 바로 확인할 수 있어요",
3124
3255
  "댓글 수는 모든 목록 보기에서 제목 옆에 같은 방식으로 보여요"
3256
+ ],
3257
+ highlightsEn: [
3258
+ "Posts now have view counts, which count even without signing in",
3259
+ "See view counts right in the list and on the post",
3260
+ "Comment counts show next to the title the same way in every list view"
3125
3261
  ]
3126
3262
  },
3127
3263
  {
@@ -3132,22 +3268,31 @@ var ci = [
3132
3268
  "이미지 목록에서는 사진 위에 제목과 좋아요가 겹쳐 보여요",
3133
3269
  "영상 목록에서는 유튜브가 있으면 유튜브 썸네일을 먼저 보여 줘요",
3134
3270
  "시간·설명 툴팁이 마우스만 올려도 바로 떠요"
3271
+ ],
3272
+ highlightsEn: [
3273
+ "A new dense list view lets you scan posts one line at a time",
3274
+ "The image list overlays the title and likes on the photo",
3275
+ "The video list shows the YouTube thumbnail first when there is one",
3276
+ "Time and description tooltips appear as soon as you hover"
3135
3277
  ]
3136
3278
  },
3137
3279
  {
3138
3280
  version: "0.15.0",
3139
3281
  date: "2026-08-06",
3140
- highlights: ["게시판마다 짧은 설명을 적어 두고, 제목에 마우스를 올리면 볼 수 있어요", "일반 목록 카드 여백과 썸네일 배치를 더 단정하게 다듬었어요"]
3282
+ highlights: ["게시판마다 짧은 설명을 적어 두고, 제목에 마우스를 올리면 볼 수 있어요", "일반 목록 카드 여백과 썸네일 배치를 더 단정하게 다듬었어요"],
3283
+ highlightsEn: ["Give each board a short description, shown when you hover over its title", "Tidied up the spacing and thumbnail layout of default list cards"]
3141
3284
  },
3142
3285
  {
3143
3286
  version: "0.14.1",
3144
3287
  date: "2026-08-06",
3145
- highlights: ["일반 리스트뷰에 정사각형 썸네일이 생기고, 작성자·시간·좋아요·댓글수를 오른쪽에 보기 좋게 정리했어요", "카테고리 안에서는 같은 카테고리 뱃지가 매 글마다 반복 표시되지 않아요"]
3288
+ highlights: ["일반 리스트뷰에 정사각형 썸네일이 생기고, 작성자·시간·좋아요·댓글수를 오른쪽에 보기 좋게 정리했어요", "카테고리 안에서는 같은 카테고리 뱃지가 매 글마다 반복 표시되지 않아요"],
3289
+ highlightsEn: ["The default list view gains square thumbnails, with author, time, likes and comment count neatly on the right", "Inside a category, the same category badge no longer repeats on every post"]
3146
3290
  },
3147
3291
  {
3148
3292
  version: "0.14.0",
3149
3293
  date: "2026-08-06",
3150
- highlights: ["게시판 설정에서 글쓰기·댓글쓰기 권한과 리스트뷰를 카드로 골라요", "목록 위에서 일반·이미지·영상 보기를 아이콘으로 바로 바꿔볼 수 있어요"]
3294
+ highlights: ["게시판 설정에서 글쓰기·댓글쓰기 권한과 리스트뷰를 카드로 골라요", "목록 위에서 일반·이미지·영상 보기를 아이콘으로 바로 바꿔볼 수 있어요"],
3295
+ highlightsEn: ["Pick write and comment permissions and the list view as cards in board settings", "Switch between default, image and video views with icons above the list"]
3151
3296
  },
3152
3297
  {
3153
3298
  version: "0.13.0",
@@ -3156,6 +3301,11 @@ var ci = [
3156
3301
  "게시판마다 담당 스태프를 따로 지정할 수 있어요",
3157
3302
  "댓글쓰기 권한도 게시판마다 다르게 정할 수 있어요",
3158
3303
  "유튜브 영상이 모바일과 여러 주소 형식에서도 잘 보여요"
3304
+ ],
3305
+ highlightsEn: [
3306
+ "Assign staff to each board separately",
3307
+ "Set comment permissions per board too",
3308
+ "YouTube videos play properly on mobile and with more URL formats"
3159
3309
  ]
3160
3310
  },
3161
3311
  {
@@ -3165,45 +3315,58 @@ var ci = [
3165
3315
  "게시글에 좋아요 버튼이 생겼어요",
3166
3316
  "글을 보는 동안 다른 사람의 좋아요·댓글 수가 화면에 바로 나타나요",
3167
3317
  "작성 시간을 몇 분 전처럼 쉽게 보고, 올리면 정확한 시각을 확인할 수 있어요"
3318
+ ],
3319
+ highlightsEn: [
3320
+ "Posts have a like button",
3321
+ "Other people's likes and comment counts update on screen while you read",
3322
+ "See times like \"5 minutes ago\" and hover for the exact time"
3168
3323
  ]
3169
3324
  },
3170
3325
  {
3171
3326
  version: "0.10.0",
3172
3327
  date: "2026-08-05",
3173
- highlights: ["게시판 설치와 연결 설정이 한결 단순해졌어요", "게시판 아래에서 제작 정보와 지금까지의 버전 이야기를 볼 수 있어요"]
3328
+ highlights: ["게시판 설치와 연결 설정이 한결 단순해졌어요", "게시판 아래에서 제작 정보와 지금까지의 버전 이야기를 볼 수 있어요"],
3329
+ highlightsEn: ["Installing and connecting the board is much simpler", "See credits and the version story at the bottom of the board"]
3174
3330
  },
3175
3331
  {
3176
3332
  version: "0.9.0",
3177
3333
  date: "2026-08-04",
3178
- highlights: ["댓글에 답글을 달고 대화 흐름을 볼 수 있어요", "댓글을 이어서 불러오고, 새 댓글은 실시간으로 받아요"]
3334
+ highlights: ["댓글에 답글을 달고 대화 흐름을 볼 수 있어요", "댓글을 이어서 불러오고, 새 댓글은 실시간으로 받아요"],
3335
+ highlightsEn: ["Reply to comments and follow the conversation", "Load more comments as you go and get new ones in real time"]
3179
3336
  },
3180
3337
  {
3181
3338
  version: "0.8.0",
3182
3339
  date: "2026-08-04",
3183
- highlights: ["최근 댓글은 실시간으로 보고 이전 댓글은 이어서 불러올 수 있어요", "댓글 작성 영역을 대화 흐름의 맨 아래로 옮겼어요"]
3340
+ highlights: ["최근 댓글은 실시간으로 보고 이전 댓글은 이어서 불러올 수 있어요", "댓글 작성 영역을 대화 흐름의 맨 아래로 옮겼어요"],
3341
+ highlightsEn: ["See recent comments in real time and load older ones as you go", "Moved the comment box to the bottom of the conversation"]
3184
3342
  },
3185
3343
  {
3186
3344
  version: "0.7.0",
3187
3345
  date: "2026-08-04",
3188
- highlights: ["카테고리마다 글쓰기 권한을 일반 이상·스태프 이상·관리자만으로 설정할 수 있어요", "게시판 설정 화면의 SSR hydration 경고를 해결했어요"]
3346
+ highlights: ["카테고리마다 글쓰기 권한을 일반 이상·스태프 이상·관리자만으로 설정할 수 있어요", "게시판 설정 화면의 SSR hydration 경고를 해결했어요"],
3347
+ highlightsEn: ["Set write permission per category to users and up, staff and up, or admins only", "Fixed the SSR hydration warning on the board settings screen"]
3189
3348
  },
3190
3349
  {
3191
3350
  version: "0.6.0",
3192
3351
  date: "2026-08-04",
3193
- highlights: ["관리자가 게시판 사용자를 확인하고 관리자·스태프·사용자 역할을 설정할 수 있어요", "스태프는 게시판 설정이나 사용자 역할은 바꾸지 않고 게시글과 댓글을 관리할 수 있어요"]
3352
+ highlights: ["관리자가 게시판 사용자를 확인하고 관리자·스태프·사용자 역할을 설정할 수 있어요", "스태프는 게시판 설정이나 사용자 역할은 바꾸지 않고 게시글과 댓글을 관리할 수 있어요"],
3353
+ highlightsEn: ["Admins can review board users and set admin, staff and user roles", "Staff can manage posts and comments without changing board settings or user roles"]
3194
3354
  },
3195
3355
  {
3196
3356
  version: "0.5.0",
3197
3357
  date: "2026-08-04",
3198
- highlights: ["게시판마다 일반·이미지·영상 목록을 고르고, 유튜브를 본문에 넣을 수 있어요", "이전 글·목록·다음 글 이동과 카테고리 관리를 지원해요"]
3358
+ highlights: ["게시판마다 일반·이미지·영상 목록을 고르고, 유튜브를 본문에 넣을 수 있어요", "이전 글·목록·다음 글 이동과 카테고리 관리를 지원해요"],
3359
+ highlightsEn: ["Choose a default, image or video list per board, and embed YouTube in posts", "Move to the previous post, the list or the next post, and manage categories"]
3199
3360
  },
3200
3361
  {
3201
3362
  version: "0.2.0",
3202
3363
  date: "2026-07-31",
3203
3364
  highlights: ["글쓰기·댓글·첨부·로그인 권한으로 게시판을 바로 시작할 수 있어요", "부적절한 글·댓글은 AI 검열로 막을 수 있어요"],
3204
- note: "실제 보안은 호스트 Firebase 규칙 등록이 필요합니다."
3365
+ highlightsEn: ["Start a board right away with posting, comments, attachments and sign-in permissions", "Block inappropriate posts and comments with AI moderation"],
3366
+ note: "실제 보안은 호스트 Firebase 규칙 등록이 필요합니다.",
3367
+ noteEn: "Real security requires registering the host's Firebase rules."
3205
3368
  }
3206
- ], li = 10, ui = 864e5, di = 200, fi = {
3369
+ ], di = 10, fi = 864e5, pi = 200, mi = {
3207
3370
  proofread: "맞춤법, 띄어쓰기, 문법 오류만 고쳐라. 의미와 말투는 바꾸지 마라.",
3208
3371
  polish: "뜻과 작성자의 말투를 유지하면서 더 자연스럽고 읽기 좋은 문장으로 다듬어라.",
3209
3372
  continue: "앞의 맥락과 말투에 맞는 내용을 과장 없이 한두 문단 이어서 작성하라.",
@@ -3212,7 +3375,7 @@ var ci = [
3212
3375
  "translate-ko": "자연스러운 한국어로 번역하라. 고유명사와 기술 용어는 정확히 유지하라.",
3213
3376
  title: "본문을 대표하는 간결한 한국어 제목 하나를 만들어라. 따옴표나 설명 없이 제목만 출력하라."
3214
3377
  };
3215
- function pi(e) {
3378
+ function hi(e) {
3216
3379
  return {
3217
3380
  proofread: "Fix only spelling, spacing, and grammar errors. Do not change the meaning or tone.",
3218
3381
  polish: "Refine the text into more natural, readable sentences while keeping the meaning and the author's tone.",
@@ -3223,27 +3386,27 @@ function pi(e) {
3223
3386
  title: `Create one concise title in ${e} that represents the body. Output only the title, without quotes or explanation.`
3224
3387
  };
3225
3388
  }
3226
- function mi(e, t, n) {
3227
- let r = t === "ko", i = xn(t);
3389
+ function gi(e, t, n) {
3390
+ let r = t === "ko", i = Cn(t);
3228
3391
  if (e.action !== "custom") {
3229
- if (r) return fi[e.action];
3230
- let t = pi(i)[e.action];
3392
+ if (r) return mi[e.action];
3393
+ let t = hi(i)[e.action];
3231
3394
  return e.action === "translate-en" || e.action === "translate-ko" || e.action === "title" ? t : `${t} Write the result in the same language as the original text; if unclear, write in ${i}.`;
3232
3395
  }
3233
- let a = e.customInstruction?.trim().slice(0, di);
3396
+ let a = e.customInstruction?.trim().slice(0, pi);
3234
3397
  if (!a) throw Error(n("writingAssistant.customRequired"));
3235
3398
  return r ? `뜻은 유지하면서 다음 스타일 지시를 반영해 다시 써라: ${a}` : `Rewrite the text applying the following style instruction while keeping the meaning: ${a}`;
3236
3399
  }
3237
- function hi(e) {
3400
+ function _i(e) {
3238
3401
  return e.trim().replace(/^```(?:html|text)?\s*/i, "").replace(/\s*```$/, "").trim();
3239
3402
  }
3240
- function gi(e) {
3403
+ function vi(e) {
3241
3404
  return !e || typeof e != "object" ? null : "toMillis" in e && typeof e.toMillis == "function" ? e.toMillis() : null;
3242
3405
  }
3243
- function _i(e = {}) {
3244
- let t = j(), n = P(() => N(L(e.locale)) ?? t.locale.value), r = (e, t) => A(n.value, e, t), i = Me(), a = Z(), o = Ae(), s = v(), { translateEntries: c, translateHtml: l } = Bn();
3406
+ function yi(e = {}) {
3407
+ let t = j(), n = P(() => N(L(e.locale)) ?? t.locale.value), r = (e, t) => A(n.value, e, t), i = Me(), a = Z(), o = Ae(), s = v(), { translateEntries: c, translateHtml: l } = Hn();
3245
3408
  function u(e, t) {
3246
- let n = gi(e.aiWritingWindowAt), r = n != null && t - n < ui, i = Math.max(0, Math.floor(Number(e.aiWritingCount) || 0)), a = Math.max(0, Math.floor(Number(e.aiWritingPending) || 0));
3409
+ let n = vi(e.aiWritingWindowAt), r = n != null && t - n < fi, i = Math.max(0, Math.floor(Number(e.aiWritingCount) || 0)), a = Math.max(0, Math.floor(Number(e.aiWritingPending) || 0));
3247
3410
  return {
3248
3411
  active: r,
3249
3412
  windowAt: n,
@@ -3252,7 +3415,7 @@ function _i(e = {}) {
3252
3415
  };
3253
3416
  }
3254
3417
  function d(e, t) {
3255
- let n = e + ui, i = Math.max(1, Math.ceil((n - t) / 36e5));
3418
+ let n = e + fi, i = Math.max(1, Math.ceil((n - t) / 36e5));
3256
3419
  return Error(r("writingAssistant.dailyLimit", {
3257
3420
  limit: 10,
3258
3421
  hours: i
@@ -3304,7 +3467,7 @@ function _i(e = {}) {
3304
3467
  }
3305
3468
  async function y(e) {
3306
3469
  (e.action === "translate-en" || e.action === "translate-ko") && D();
3307
- let t = n.value, a = t === "ko", o = mi(e, t, r);
3470
+ let t = n.value, a = t === "ko", o = gi(e, t, r);
3308
3471
  if (!e.content.trim()) throw Error(r("writingAssistant.contentRequired"));
3309
3472
  return _(async () => {
3310
3473
  let t = s.moderation ?? {}, n = Xe(i, {
@@ -3333,50 +3496,57 @@ function _i(e = {}) {
3333
3496
  `Task: ${o}`,
3334
3497
  e.title ? `Current title: ${e.title}` : "",
3335
3498
  `Body:\n${l.slice(0, 12e3)}`
3336
- ]).filter(Boolean).join("\n\n"), d = hi((await c.generateContent(u)).response.text());
3499
+ ]).filter(Boolean).join("\n\n"), d = _i((await c.generateContent(u)).response.text());
3337
3500
  if (!d) throw Error(r("writingAssistant.emptyResult"));
3338
3501
  return d;
3339
3502
  });
3340
3503
  }
3341
3504
  async function b(e) {
3342
3505
  D();
3343
- let t = e.source ?? n.value, i = e.title?.trim() ?? "", a = e.summary?.trim() ?? "", o = e.content ?? "", s = {};
3344
- i && (s.title = i), a && (s.summary = a);
3345
- let u = !!on(o);
3346
- if (!Object.keys(s).length && !u) throw Error(r("writingAssistant.translateEmpty"));
3347
- if (u && o.length > 12e3) throw Error(r("writingAssistant.translateTooLong", { limit: Fn }));
3506
+ let t = e.source ?? n.value, i = e.targets?.length ? e.targets : pt(), a = [...i, t], o = e.title?.trim() ?? "", s = e.summary?.trim() ?? "", u = e.content ?? "", d = {};
3507
+ o && (d.title = o), s && (d.summary = s);
3508
+ let f = !!cn(u);
3509
+ if (!Object.keys(d).length && !f) throw Error(r("writingAssistant.translateEmpty"));
3510
+ if (f && u.length > 12e3) throw Error(r("writingAssistant.translateTooLong", {
3511
+ limit: Ln,
3512
+ count: i.length
3513
+ }));
3348
3514
  return _(async () => {
3349
- let [e, n] = await Promise.all([Object.keys(s).length ? c([{
3515
+ let [e, n] = await Promise.all([Object.keys(d).length ? c([{
3350
3516
  id: "post",
3351
- fields: s,
3517
+ fields: d,
3352
3518
  context: "message board post title and short summary"
3353
3519
  }], {
3354
3520
  source: t,
3521
+ targets: i,
3355
3522
  purpose: "message board post title and short summary"
3356
- }) : Promise.resolve({}), u ? l(o, { source: t }) : Promise.resolve({})]), d = ft(t, s, e.post);
3357
- if (Object.keys(s).length && !pt(d, Object.keys(s))) throw Error(r("writingAssistant.translateIncomplete"));
3358
- let f = {
3359
- i18n: d,
3523
+ }) : Promise.resolve({}), f ? l(u, {
3524
+ source: t,
3525
+ targets: i
3526
+ }) : Promise.resolve({})]), p = ft(t, d, e.post);
3527
+ if (Object.keys(d).length && !ht(p, Object.keys(d), a)) throw Error(r("writingAssistant.translateIncomplete"));
3528
+ let m = {
3529
+ i18n: p,
3360
3530
  i18nSource: {
3361
3531
  locale: t,
3362
- title: i,
3363
- summary: a
3532
+ title: o,
3533
+ summary: s
3364
3534
  }
3365
3535
  };
3366
- if (u) {
3536
+ if (f) {
3367
3537
  let e = {};
3368
3538
  for (let [t, r] of Object.entries(n)) {
3369
3539
  let n = N(t);
3370
3540
  n && typeof r == "string" && r.trim() && (e[n] = { content: r.trim() });
3371
3541
  }
3372
- let i = ft(t, { content: o }, e);
3373
- if (!pt(i, ["content"])) throw Error(r("writingAssistant.translateIncomplete"));
3374
- f.contentI18n = i, f.contentI18nSource = {
3542
+ let i = ft(t, { content: u }, e);
3543
+ if (!ht(i, ["content"], a)) throw Error(r("writingAssistant.translateIncomplete"));
3544
+ m.contentI18n = i, m.contentI18nSource = {
3375
3545
  locale: t,
3376
- hash: ct(o)
3546
+ hash: ct(u)
3377
3547
  };
3378
3548
  }
3379
- return f;
3549
+ return m;
3380
3550
  });
3381
3551
  }
3382
3552
  return {
@@ -3386,14 +3556,14 @@ function _i(e = {}) {
3386
3556
  }
3387
3557
  //#endregion
3388
3558
  //#region src/utils/renderMarkdown.ts
3389
- function vi(e) {
3559
+ function bi(e) {
3390
3560
  return e.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
3391
3561
  }
3392
- function yi(e) {
3562
+ function xi(e) {
3393
3563
  let t = e;
3394
3564
  return t = t.replace(/`([^`]+)`/g, "<code>$1</code>"), t = t.replace(/!\[([^\]]*)\]\((https?:\/\/[^)\s]+)\)/g, "<img src=\"$2\" alt=\"$1\" class=\"max-w-full h-auto rounded-lg my-3\" loading=\"lazy\" />"), t = t.replace(/\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g, "<a href=\"$2\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"text-primary underline\">$1</a>"), t = t.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>"), t = t.replace(/\*([^*]+)\*/g, "<em>$1</em>"), t = t.replace(/~~([^~]+)~~/g, "<del>$1</del>"), t;
3395
3565
  }
3396
- function bi(e) {
3566
+ function Si(e) {
3397
3567
  if (!e?.trim()) return "";
3398
3568
  let t = e.replace(/\r\n/g, "\n").split("\n"), n = [], r = 0, i = !1, a = [], o = null, s = [];
3399
3569
  function c() {
@@ -3402,7 +3572,7 @@ function bi(e) {
3402
3572
  return;
3403
3573
  }
3404
3574
  let e = o;
3405
- n.push(`<${e}>${s.map((e) => `<li>${yi(e)}</li>`).join("")}</${e}>`), o = null, s = [];
3575
+ n.push(`<${e}>${s.map((e) => `<li>${xi(e)}</li>`).join("")}</${e}>`), o = null, s = [];
3406
3576
  }
3407
3577
  for (; r < t.length;) {
3408
3578
  let e = t[r] ?? "", l = e.match(/^```(\w*)\s*$/);
@@ -3411,7 +3581,7 @@ function bi(e) {
3411
3581
  continue;
3412
3582
  }
3413
3583
  if (i) {
3414
- a.push(vi(e)), r++;
3584
+ a.push(bi(e)), r++;
3415
3585
  continue;
3416
3586
  }
3417
3587
  if (/^(-{3,}|\*{3,}|_{3,})\s*$/.test(e)) {
@@ -3422,24 +3592,24 @@ function bi(e) {
3422
3592
  if (u) {
3423
3593
  c();
3424
3594
  let e = u[1].length;
3425
- n.push(`<h${e} class="font-semibold mt-4 mb-2">${yi(vi(u[2]))}</h${e}>`), r++;
3595
+ n.push(`<h${e} class="font-semibold mt-4 mb-2">${xi(bi(u[2]))}</h${e}>`), r++;
3426
3596
  continue;
3427
3597
  }
3428
3598
  if (e.startsWith("> ")) {
3429
3599
  c();
3430
3600
  let e = [];
3431
- for (; r < t.length && (t[r] ?? "").startsWith("> ");) e.push(vi((t[r] ?? "").slice(2))), r++;
3432
- n.push(`<blockquote class="border-l-4 border-default pl-3 text-muted my-3">${yi(e.join("<br />"))}</blockquote>`);
3601
+ for (; r < t.length && (t[r] ?? "").startsWith("> ");) e.push(bi((t[r] ?? "").slice(2))), r++;
3602
+ n.push(`<blockquote class="border-l-4 border-default pl-3 text-muted my-3">${xi(e.join("<br />"))}</blockquote>`);
3433
3603
  continue;
3434
3604
  }
3435
3605
  let d = e.match(/^[-*+]\s+(.+)$/);
3436
3606
  if (d) {
3437
- o && o !== "ul" && c(), o = "ul", s.push(vi(d[1])), r++;
3607
+ o && o !== "ul" && c(), o = "ul", s.push(bi(d[1])), r++;
3438
3608
  continue;
3439
3609
  }
3440
3610
  let f = e.match(/^\d+\.\s+(.+)$/);
3441
3611
  if (f) {
3442
- o && o !== "ol" && c(), o = "ol", s.push(vi(f[1])), r++;
3612
+ o && o !== "ol" && c(), o = "ol", s.push(bi(f[1])), r++;
3443
3613
  continue;
3444
3614
  }
3445
3615
  if (!e.trim()) {
@@ -3451,22 +3621,22 @@ function bi(e) {
3451
3621
  for (; r < t.length;) {
3452
3622
  let e = t[r] ?? "";
3453
3623
  if (!e.trim() || /^(#{1,3}\s|```|[-*+]\s|\d+\.\s|> |(-{3,}|\*{3,})\s*$)/.test(e)) break;
3454
- p.push(vi(e)), r++;
3624
+ p.push(bi(e)), r++;
3455
3625
  }
3456
- n.push(`<p class="my-2 leading-relaxed">${yi(p.join("<br />"))}</p>`);
3626
+ n.push(`<p class="my-2 leading-relaxed">${xi(p.join("<br />"))}</p>`);
3457
3627
  }
3458
3628
  return c(), i && a.length && n.push(`<pre class="overflow-x-auto rounded-lg bg-muted p-3 text-sm"><code>${a.join("\n")}</code></pre>`), n.join("\n");
3459
3629
  }
3460
3630
  //#endregion
3461
3631
  //#region src/utils/orphanCleanup.ts
3462
- function xi(e, t, n, r) {
3632
+ function Ci(e, t, n, r) {
3463
3633
  return typeof t.postId != "string" || !t.postId || t.postId.includes("/") ? !1 : !n.has(t.postId) || e === "like" && t.target === "comment" && typeof t.commentId == "string" && !!t.commentId && !t.commentId.includes("/") && !r.has(t.commentId);
3464
3634
  }
3465
- function Si(e, t) {
3635
+ function wi(e, t) {
3466
3636
  let n = Date.parse(e);
3467
3637
  return Number.isFinite(n) && t - n >= 864e5;
3468
3638
  }
3469
- function Ci(e, t) {
3639
+ function Ti(e, t) {
3470
3640
  if (!e.startsWith(`${t}/`)) return null;
3471
3641
  let n = e.slice(t.length + 1).split("/");
3472
3642
  if (n.some((e) => !e || e === "." || e === "..")) return null;
@@ -3476,14 +3646,14 @@ function Ci(e, t) {
3476
3646
  commentId: a
3477
3647
  } : null;
3478
3648
  }
3479
- function wi(e, t, n) {
3649
+ function Ei(e, t, n) {
3480
3650
  let r = `${t === "comment" ? n.commentsCollection : n.likesCollection}/`, i = e.startsWith(r) ? e.slice(r.length) : "";
3481
3651
  return !!i && !i.includes("/") && i !== "." && i !== "..";
3482
3652
  }
3483
3653
  //#endregion
3484
3654
  //#region src/composables/useMemiBoardCleanup.ts
3485
- function Ti() {
3486
- let e = Z(), t = Me(), { isAdmin: n, rolePending: r } = Vt(), { t: i } = j(), a = I([]), o = I(!1), s = I(!1), c = I(""), l = I(""), u = [], d = "", f = /* @__PURE__ */ new Map();
3655
+ function Di() {
3656
+ let e = Z(), t = Me(), { isAdmin: n, rolePending: r } = Ut(), { t: i } = j(), a = I([]), o = I(!1), s = I(!1), c = I(""), l = I(""), u = [], d = "", f = /* @__PURE__ */ new Map();
3487
3657
  function p() {
3488
3658
  let e = Le(t).currentUser?.uid;
3489
3659
  if (!e) throw Error(i("settings.noAccess"));
@@ -3504,10 +3674,10 @@ function Ti() {
3504
3674
  function o(e) {
3505
3675
  if (typeof e == "string") {
3506
3676
  e.startsWith(`${t.postsCollection}/`) && a.add(e);
3507
- let n = Jt(e);
3677
+ let n = Xt(e);
3508
3678
  n && a.add(n);
3509
3679
  for (let t of e.matchAll(/https?:\/\/[^\s"'<>]+/g)) {
3510
- let e = Jt(t[0].replace(/&amp;/g, "&"));
3680
+ let e = Xt(t[0].replace(/&amp;/g, "&"));
3511
3681
  e && a.add(e);
3512
3682
  }
3513
3683
  } else Array.isArray(e) ? e.forEach(o) : e && typeof e == "object" && Object.getPrototypeOf(e) === Object.prototype && Object.values(e).forEach(o);
@@ -3537,7 +3707,7 @@ function Ti() {
3537
3707
  let r = [];
3538
3708
  for (let [e, t] of [["comment", n.commentDocs], ["like", n.likes]]) for (let i of t) {
3539
3709
  let t = i.data();
3540
- xi(e, t, n.posts, n.comments) && r.push({
3710
+ Ci(e, t, n.posts, n.comments) && r.push({
3541
3711
  kind: e,
3542
3712
  path: i.ref.path,
3543
3713
  postId: t.postId,
@@ -3555,12 +3725,12 @@ function Ti() {
3555
3725
  });
3556
3726
  o.push(...e.prefixes);
3557
3727
  for (let t of e.items) {
3558
- let e = Ci(t.fullPath, n.cfg.postsCollection);
3728
+ let e = Ti(t.fullPath, n.cfg.postsCollection);
3559
3729
  if (!e) continue;
3560
3730
  let { postId: i, commentId: a } = e;
3561
3731
  if (n.referenced.has(t.fullPath) || n.posts.has(i) && (!a || n.comments.has(a))) continue;
3562
3732
  let o = await Ue(t);
3563
- Si(o.updated, Date.now()) && (f.set(t.fullPath, o.generation), r.push({
3733
+ wi(o.updated, Date.now()) && (f.set(t.fullPath, o.generation), r.push({
3564
3734
  kind: "file",
3565
3735
  path: t.fullPath,
3566
3736
  postId: i,
@@ -3593,23 +3763,23 @@ function Ti() {
3593
3763
  try {
3594
3764
  let i = !1;
3595
3765
  if (n.kind === "file") {
3596
- if (!Ci(n.path, o.cfg.postsCollection)) throw Error("Invalid cleanup path");
3766
+ if (!Ti(n.path, o.cfg.postsCollection)) throw Error("Invalid cleanup path");
3597
3767
  let r = We(t), a = qe(r, n.path), [s, c, l] = await Promise.all([
3598
3768
  Se(z(e, o.cfg.postsCollection, n.postId)),
3599
3769
  n.commentId ? Se(z(e, o.cfg.commentsCollection, n.commentId)) : null,
3600
3770
  Ue(a)
3601
3771
  ]);
3602
- m(d), l.generation === f.get(n.path) && !o.referenced.has(n.path) && Si(l.updated, Date.now()) && (!s.exists() || n.commentId && !c?.exists()) && (await He(a), i = !0);
3772
+ m(d), l.generation === f.get(n.path) && !o.referenced.has(n.path) && wi(l.updated, Date.now()) && (!s.exists() || n.commentId && !c?.exists()) && (await He(a), i = !0);
3603
3773
  } else {
3604
3774
  let t = n.kind;
3605
- if (!wi(n.path, t, o.cfg)) throw Error("Invalid cleanup path");
3775
+ if (!Ei(n.path, t, o.cfg)) throw Error("Invalid cleanup path");
3606
3776
  i = await Ee(e, async (r) => {
3607
3777
  let i = await r.get(z(e, n.path));
3608
3778
  if (!i.exists()) return !0;
3609
3779
  let a = i.data();
3610
3780
  if (typeof a.postId != "string" || !a.postId || a.postId.includes("/")) return !1;
3611
3781
  let s = await r.get(z(e, o.cfg.postsCollection, a.postId)), c = n.kind === "like" && a.target === "comment" && typeof a.commentId == "string" && a.commentId && !a.commentId.includes("/") ? await r.get(z(e, o.cfg.commentsCollection, a.commentId)) : null;
3612
- return xi(t, a, new Set(s.exists() ? [a.postId] : []), new Set(c?.exists() ? [a.commentId] : [])) ? (m(d), r.delete(i.ref), !0) : !1;
3782
+ return Ci(t, a, new Set(s.exists() ? [a.postId] : []), new Set(c?.exists() ? [a.commentId] : [])) ? (m(d), r.delete(i.ref), !0) : !1;
3613
3783
  });
3614
3784
  }
3615
3785
  i ? r++ : p++, a.value = a.value.filter((e) => e.path !== n.path);
@@ -3639,6 +3809,6 @@ function Ti() {
3639
3809
  };
3640
3810
  }
3641
3811
  //#endregion
3642
- export { or as AUTHOR_MEMO_MAX_LENGTH, jr as BOARD_SECTION_COLS, Pr as BOARD_SECTION_KINDS, Fr as BOARD_SECTION_SORTS, Or as BOARD_USER_ROLES, fr as COMMENT_BODY_MAX_LENGTH, Nt as DEFAULT_BLOCK_BAN_DECAY_MS, Mt as DEFAULT_BLOCK_BAN_THRESHOLD, Vn as DEFAULT_BOARDS, Hn as DEFAULT_CATEGORIES, i as DEFAULT_COMMENTS_COLLECTION, r as DEFAULT_LIKES_COLLECTION, ne as DEFAULT_MEMI_BOARD_LOCALE, m as DEFAULT_POSTS_COLLECTION, ie as DEFAULT_REPORTS_COLLECTION, e as DEFAULT_SETTINGS_COLLECTION, re as DEFAULT_USERS_COLLECTION, k as FALLBACK_MEMI_BOARD_LOCALE, Fn as HTML_MAX_CHARS, T as MEMI_BOARD_LOCALES, yr as REPORT_DETAIL_MAX_LENGTH, br as REPORT_REASONS, li as WRITING_ASSISTANT_DAILY_LIMIT, $e as asHttpUrl, yt as boardCopyIsTranslated, at as boardListOgDescription, it as boardListOgTitle, rt as boardPostOgDescription, nt as boardPostOgTitle, Mr as boardSectionKinds, Nr as boardSectionSorts, p as boardSsrKey, h as boardUserDoc, c as boardUsersCol, mn as buildPostPreview, Dt as canManageBoardByRole, Ot as canWriteCommentByRole, Lr as clampSectionCount, d as commentDoc, g as commentsCol, pe as configureMemiBoard, pt as coversLocales, Ir as defaultSectionCount, vt as displayPostContent, _t as displayPostSummary, gt as displayPostTitle, ht as displayStoredField, Pt as effectiveModerationBlockCount, qt as extractEditorImageUrls, wt as fetchPublicListForSeo, Ct as fetchPublicPostForSeo, ee as formatDate, ae as formatFullDate, de as formatRelativeDate, Lt as formatRestrictedUntilLabel, O as formatTimestampDetails, ln as hasBodyImage, cn as hasBodyText, ct as hashText, Gt as imeSafeSubmitClick, Wt as imeSafeSubmitPointerDown, Et as isAssignedStaff, un as isContentEmpty, qn as isFirestorePermissionDenied, _ as isMemiBoardI18nEnabled, te as isMemiBoardLocaleLoaded, n as isMemiBoardTranslationEnabled, It as isModerationWriteRestricted, me as likeDoc, t as likeDocId, b as likesCol, se as loadMemiBoardLocale, mt as localizedText, _r as memiBoardCommentLikesKey, oe as memiBoardHostLocaleKey, fe as memiBoardLocaleInfo, ft as mergeSourceFields, Jr as miniBoardView, Ur as miniCommentLink, Hr as miniMoreLink, Wr as miniPostImage, Vr as miniPostLink, Gr as miniPostTitle, Yr as miniPostsView, Kr as miniViewGroup, qr as miniViewLabel, Ft as moderationWriteRestrictedUntilMs, ot as normalizeBasePath, ii as normalizeBoardSection, N as normalizeMemiBoardLocale, Rr as pairSectionCols, on as plainTextFromHtml, s as postBodyDoc, y as postDoc, Yt as postNamespaceFromStoragePath, x as postStorageFolder, o as postsCol, ut as readI18nSource, Q as readLocalizedFields, bi as renderMarkdownToHtml, u as reportDoc, ce as reportDocId, xr as reportReasonLabel, l as reportsCol, C as resolveBoardPathConfig, bt as resolvePublicSeoDb, zr as sectionColClass, Br as sectionMinHeight, S as settingsCol, w as settingsDoc, E as slugify, Jt as storagePathFromDownloadUrl, sn as titleFromBody, tt as toAbsoluteUrl, et as toBoardOgImageUrl, A as translate, M as useBoardPathConfig, Vt as useMemiBoardAuth, cr as useMemiBoardAuthorMemo, Bn as useMemiBoardAutoTranslate, Ti as useMemiBoardCleanup, ar as useMemiBoardCommentFeed, vr as useMemiBoardCommentLikes, mr as useMemiBoardComments, v as useMemiBoardConfig, j as useMemiBoardI18n, gr as useMemiBoardLikes, oi as useMemiBoardMiniLatest, si as useMemiBoardMiniPicked, Dr as useMemiBoardModeration, $n as useMemiBoardPost, Qn as useMemiBoardPostList, Yn as useMemiBoardPosts, hr as useMemiBoardReplies, wr as useMemiBoardReportQueue, Sr as useMemiBoardReports, ai as useMemiBoardSections, Gn as useMemiBoardSettings, rr as useMemiBoardUserComments, tr as useMemiBoardUserPosts, Ar as useMemiBoardUserProfile, kr as useMemiBoardUsers, Tr as useMemiBoardViews, _i as useMemiBoardWritingAssistant, ci as versionHistory, pn as videoListCoverUrl, fn as youtubeId };
3812
+ export { cr as AUTHOR_MEMO_MAX_LENGTH, Nr as BOARD_SECTION_COLS, Ir as BOARD_SECTION_KINDS, Lr as BOARD_SECTION_SORTS, Ar as BOARD_USER_ROLES, mr as COMMENT_BODY_MAX_LENGTH, Ft as DEFAULT_BLOCK_BAN_DECAY_MS, Pt as DEFAULT_BLOCK_BAN_THRESHOLD, Un as DEFAULT_BOARDS, Wn as DEFAULT_CATEGORIES, i as DEFAULT_COMMENTS_COLLECTION, r as DEFAULT_LIKES_COLLECTION, ne as DEFAULT_MEMI_BOARD_LOCALE, m as DEFAULT_POSTS_COLLECTION, ie as DEFAULT_REPORTS_COLLECTION, e as DEFAULT_SETTINGS_COLLECTION, re as DEFAULT_USERS_COLLECTION, k as FALLBACK_MEMI_BOARD_LOCALE, Ln as HTML_MAX_CHARS, T as MEMI_BOARD_LOCALES, xr as REPORT_DETAIL_MAX_LENGTH, Sr as REPORT_REASONS, di as WRITING_ASSISTANT_DAILY_LIMIT, $e as asHttpUrl, xt as boardCopyIsTranslated, at as boardListOgDescription, it as boardListOgTitle, rt as boardPostOgDescription, nt as boardPostOgTitle, Pr as boardSectionKinds, Fr as boardSectionSorts, p as boardSsrKey, pt as boardTranslationLocales, h as boardUserDoc, c as boardUsersCol, gn as buildPostPreview, kt as canManageBoardByRole, At as canWriteCommentByRole, zr as clampSectionCount, d as commentDoc, g as commentsCol, pe as configureMemiBoard, ht as coversLocales, Rr as defaultSectionCount, bt as displayPostContent, yt as displayPostSummary, vt as displayPostTitle, _t as displayStoredField, It as effectiveModerationBlockCount, Yt as extractEditorImageUrls, Et as fetchPublicListForSeo, Tt as fetchPublicPostForSeo, ee as formatDate, ae as formatFullDate, de as formatRelativeDate, zt as formatRestrictedUntilLabel, O as formatTimestampDetails, dn as hasBodyImage, un as hasBodyText, ct as hashText, qt as imeSafeSubmitClick, Kt as imeSafeSubmitPointerDown, Ot as isAssignedStaff, fn as isContentEmpty, Yn as isFirestorePermissionDenied, _ as isMemiBoardI18nEnabled, te as isMemiBoardLocaleLoaded, n as isMemiBoardTranslationEnabled, Rt as isModerationWriteRestricted, me as likeDoc, t as likeDocId, b as likesCol, se as loadMemiBoardLocale, gt as localizedText, yr as memiBoardCommentLikesKey, oe as memiBoardHostLocaleKey, fe as memiBoardLocaleInfo, ft as mergeSourceFields, Xr as miniBoardView, Gr as miniCommentLink, Wr as miniMoreLink, Kr as miniPostImage, Ur as miniPostLink, qr as miniPostTitle, Zr as miniPostsView, Jr as miniViewGroup, Yr as miniViewLabel, Lt as moderationWriteRestrictedUntilMs, ot as normalizeBasePath, oi as normalizeBoardSection, N as normalizeMemiBoardLocale, Br as pairSectionCols, cn as plainTextFromHtml, s as postBodyDoc, y as postDoc, Zt as postNamespaceFromStoragePath, x as postStorageFolder, o as postsCol, ut as readI18nSource, Q as readLocalizedFields, Si as renderMarkdownToHtml, u as reportDoc, ce as reportDocId, Cr as reportReasonLabel, l as reportsCol, C as resolveBoardPathConfig, St as resolvePublicSeoDb, Vr as sectionColClass, Hr as sectionMinHeight, S as settingsCol, w as settingsDoc, E as slugify, Xt as storagePathFromDownloadUrl, ln as titleFromBody, tt as toAbsoluteUrl, et as toBoardOgImageUrl, A as translate, M as useBoardPathConfig, Ut as useMemiBoardAuth, ur as useMemiBoardAuthorMemo, Hn as useMemiBoardAutoTranslate, Di as useMemiBoardCleanup, sr as useMemiBoardCommentFeed, br as useMemiBoardCommentLikes, gr as useMemiBoardComments, v as useMemiBoardConfig, j as useMemiBoardI18n, vr as useMemiBoardLikes, ci as useMemiBoardMiniLatest, li as useMemiBoardMiniPicked, kr as useMemiBoardModeration, tr as useMemiBoardPost, er as useMemiBoardPostList, Zn as useMemiBoardPosts, _r as useMemiBoardReplies, Er as useMemiBoardReportQueue, wr as useMemiBoardReports, si as useMemiBoardSections, qn as useMemiBoardSettings, ar as useMemiBoardUserComments, rr as useMemiBoardUserPosts, Mr as useMemiBoardUserProfile, jr as useMemiBoardUsers, Dr as useMemiBoardViews, yi as useMemiBoardWritingAssistant, ui as versionHistory, hn as videoListCoverUrl, mn as youtubeId };
3643
3813
 
3644
3814
  //# sourceMappingURL=index.js.map