@openrails/contentkit-ui 0.83.0 → 0.86.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 (219) hide show
  1. package/README.md +18 -7
  2. package/dist/client/beacons.d.ts +18 -6
  3. package/dist/client/beacons.js +26 -11
  4. package/dist/client/beacons.js.map +1 -1
  5. package/dist/client/client.d.ts +3 -0
  6. package/dist/client/client.js +29 -27
  7. package/dist/client/client.js.map +1 -1
  8. package/dist/client/codes.d.ts +6 -1
  9. package/dist/client/codes.js +4 -1
  10. package/dist/client/codes.js.map +1 -1
  11. package/dist/client/content/moderation.d.ts +14 -2
  12. package/dist/client/content/moderation.js +34 -0
  13. package/dist/client/content/moderation.js.map +1 -1
  14. package/dist/client/content/types.d.ts +13 -0
  15. package/dist/client/content/types.js.map +1 -1
  16. package/dist/client/discovery.d.ts +6 -0
  17. package/dist/client/discovery.js.map +1 -1
  18. package/dist/client/generated/error-codes.d.ts +1 -1
  19. package/dist/client/generated/error-codes.js +9 -1
  20. package/dist/client/generated/error-codes.js.map +1 -1
  21. package/dist/client/generated/routes.d.ts +91 -13
  22. package/dist/client/generated/routes.js +123 -2
  23. package/dist/client/generated/routes.js.map +1 -1
  24. package/dist/client/generated/wire.d.ts +81 -0
  25. package/dist/client/generated/wire.js.map +1 -1
  26. package/dist/client/index.d.ts +4 -2
  27. package/dist/client/media/queue.d.ts +4 -2
  28. package/dist/client/media/queue.js +18 -12
  29. package/dist/client/media/queue.js.map +1 -1
  30. package/dist/client/media/rules.d.ts +7 -1
  31. package/dist/client/media/rules.js +27 -21
  32. package/dist/client/media/rules.js.map +1 -1
  33. package/dist/client/reader-meta.d.ts +27 -0
  34. package/dist/client/reader-meta.js +99 -0
  35. package/dist/client/reader-meta.js.map +1 -0
  36. package/dist/client/stats.d.ts +32 -0
  37. package/dist/client/stats.js +35 -0
  38. package/dist/client/stats.js.map +1 -0
  39. package/dist/client/versions.d.ts +1 -1
  40. package/dist/client/versions.js +7 -2
  41. package/dist/client/versions.js.map +1 -1
  42. package/dist/client.js +30 -28
  43. package/dist/components/admin/content-manager.js +206 -206
  44. package/dist/components/admin/content-manager.js.map +1 -1
  45. package/dist/components/admin/group-manager.js +71 -70
  46. package/dist/components/admin/group-manager.js.map +1 -1
  47. package/dist/components/admin/media-item-panel.d.ts +24 -7
  48. package/dist/components/admin/media-item-panel.js +209 -197
  49. package/dist/components/admin/media-item-panel.js.map +1 -1
  50. package/dist/components/admin/poll-manager.js +25 -25
  51. package/dist/components/admin/reader-manifest.d.ts +18 -0
  52. package/dist/components/admin/reader-manifest.js +490 -0
  53. package/dist/components/admin/reader-manifest.js.map +1 -0
  54. package/dist/components/admin/stats-dashboard.d.ts +22 -0
  55. package/dist/components/admin/stats-dashboard.js +257 -0
  56. package/dist/components/admin/stats-dashboard.js.map +1 -0
  57. package/dist/components/content/card.js +43 -43
  58. package/dist/components/content/card.js.map +1 -1
  59. package/dist/components/content/editors.d.ts +2 -1
  60. package/dist/components/content/editors.js +479 -416
  61. package/dist/components/content/editors.js.map +1 -1
  62. package/dist/components/content/grid.js +47 -47
  63. package/dist/components/content/index.d.ts +2 -1
  64. package/dist/components/content/listing.js +34 -34
  65. package/dist/components/content/listing.js.map +1 -1
  66. package/dist/components/content/paywall.d.ts +17 -5
  67. package/dist/components/content/paywall.js +115 -100
  68. package/dist/components/content/paywall.js.map +1 -1
  69. package/dist/components/content/reader-stats.d.ts +12 -0
  70. package/dist/components/content/reader-stats.js +47 -0
  71. package/dist/components/content/reader-stats.js.map +1 -0
  72. package/dist/components/groups/banner.d.ts +1 -1
  73. package/dist/components/groups/banner.js +34 -34
  74. package/dist/components/groups/banner.js.map +1 -1
  75. package/dist/components/groups/directory.js +11 -11
  76. package/dist/components/groups/editor.js +294 -294
  77. package/dist/components/groups/editor.js.map +1 -1
  78. package/dist/components/groups/header.js +27 -27
  79. package/dist/components/groups/header.js.map +1 -1
  80. package/dist/components/groups/memberships.js +180 -180
  81. package/dist/components/groups/parts.d.ts +2 -4
  82. package/dist/components/groups/parts.js +69 -88
  83. package/dist/components/groups/parts.js.map +1 -1
  84. package/dist/components/media-folder-editor.js +239 -189
  85. package/dist/components/media-folder-editor.js.map +1 -1
  86. package/dist/components/notifications.js +17 -17
  87. package/dist/components/slot-upload.d.ts +2 -2
  88. package/dist/components/slot-upload.js +84 -79
  89. package/dist/components/slot-upload.js.map +1 -1
  90. package/dist/components/social/bans.js +34 -34
  91. package/dist/components/social/comments.js +59 -45
  92. package/dist/components/social/comments.js.map +1 -1
  93. package/dist/components/social/index.d.ts +1 -0
  94. package/dist/components/social/moderation.d.ts +5 -4
  95. package/dist/components/social/moderation.js +177 -160
  96. package/dist/components/social/moderation.js.map +1 -1
  97. package/dist/components/social/parts.js +22 -14
  98. package/dist/components/social/parts.js.map +1 -1
  99. package/dist/components/social/rules.d.ts +23 -0
  100. package/dist/components/social/rules.js +261 -0
  101. package/dist/components/social/rules.js.map +1 -0
  102. package/dist/components/video-player.d.ts +2 -0
  103. package/dist/components/video-player.js +318 -314
  104. package/dist/components/video-player.js.map +1 -1
  105. package/dist/i18n/context.d.ts +2 -0
  106. package/dist/i18n/context.js +23 -5
  107. package/dist/i18n/context.js.map +1 -1
  108. package/dist/index.d.ts +3 -1
  109. package/dist/index.js +28 -24
  110. package/dist/locales/admin/de.js +105 -0
  111. package/dist/locales/admin/de.js.map +1 -1
  112. package/dist/locales/admin/en.d.ts +105 -0
  113. package/dist/locales/admin/en.js +105 -0
  114. package/dist/locales/admin/en.js.map +1 -1
  115. package/dist/locales/admin/es.js +105 -0
  116. package/dist/locales/admin/es.js.map +1 -1
  117. package/dist/locales/admin/ja.js +105 -0
  118. package/dist/locales/admin/ja.js.map +1 -1
  119. package/dist/locales/admin/ko.js +105 -0
  120. package/dist/locales/admin/ko.js.map +1 -1
  121. package/dist/locales/admin/zh.js +105 -0
  122. package/dist/locales/admin/zh.js.map +1 -1
  123. package/dist/locales/content/de.js +6 -0
  124. package/dist/locales/content/de.js.map +1 -1
  125. package/dist/locales/content/en.d.ts +6 -0
  126. package/dist/locales/content/en.js +6 -0
  127. package/dist/locales/content/en.js.map +1 -1
  128. package/dist/locales/content/es.js +6 -0
  129. package/dist/locales/content/es.js.map +1 -1
  130. package/dist/locales/content/ja.js +6 -0
  131. package/dist/locales/content/ja.js.map +1 -1
  132. package/dist/locales/content/ko.js +6 -0
  133. package/dist/locales/content/ko.js.map +1 -1
  134. package/dist/locales/content/zh.js +6 -0
  135. package/dist/locales/content/zh.js.map +1 -1
  136. package/dist/locales/de.js +4 -1
  137. package/dist/locales/de.js.map +1 -1
  138. package/dist/locales/discovery/de.js +1 -0
  139. package/dist/locales/discovery/de.js.map +1 -1
  140. package/dist/locales/discovery/en.d.ts +1 -0
  141. package/dist/locales/discovery/en.js +1 -0
  142. package/dist/locales/discovery/en.js.map +1 -1
  143. package/dist/locales/discovery/es.js +1 -0
  144. package/dist/locales/discovery/es.js.map +1 -1
  145. package/dist/locales/discovery/ja.js +1 -0
  146. package/dist/locales/discovery/ja.js.map +1 -1
  147. package/dist/locales/discovery/ko.js +1 -0
  148. package/dist/locales/discovery/ko.js.map +1 -1
  149. package/dist/locales/discovery/zh.js +1 -0
  150. package/dist/locales/discovery/zh.js.map +1 -1
  151. package/dist/locales/en.d.ts +118 -0
  152. package/dist/locales/en.js +4 -1
  153. package/dist/locales/en.js.map +1 -1
  154. package/dist/locales/es.js +4 -1
  155. package/dist/locales/es.js.map +1 -1
  156. package/dist/locales/ja.js +4 -1
  157. package/dist/locales/ja.js.map +1 -1
  158. package/dist/locales/ko.js +4 -1
  159. package/dist/locales/ko.js.map +1 -1
  160. package/dist/locales/social/de.js +3 -0
  161. package/dist/locales/social/de.js.map +1 -1
  162. package/dist/locales/social/en.d.ts +3 -0
  163. package/dist/locales/social/en.js +3 -0
  164. package/dist/locales/social/en.js.map +1 -1
  165. package/dist/locales/social/es.js +3 -0
  166. package/dist/locales/social/es.js.map +1 -1
  167. package/dist/locales/social/ja.js +3 -0
  168. package/dist/locales/social/ja.js.map +1 -1
  169. package/dist/locales/social/ko.js +3 -0
  170. package/dist/locales/social/ko.js.map +1 -1
  171. package/dist/locales/social/zh.js +3 -0
  172. package/dist/locales/social/zh.js.map +1 -1
  173. package/dist/locales/zh.js +4 -1
  174. package/dist/locales/zh.js.map +1 -1
  175. package/dist/react/admin.d.ts +1 -1
  176. package/dist/react/admin.js +24 -23
  177. package/dist/react/admin.js.map +1 -1
  178. package/dist/react/canonical.d.ts +4 -3
  179. package/dist/react/canonical.js +34 -33
  180. package/dist/react/canonical.js.map +1 -1
  181. package/dist/react/comments.d.ts +29 -1
  182. package/dist/react/comments.js +30 -2
  183. package/dist/react/comments.js.map +1 -1
  184. package/dist/react/contents.js +144 -144
  185. package/dist/react/contents.js.map +1 -1
  186. package/dist/react/context.d.ts +1 -1
  187. package/dist/react/context.js.map +1 -1
  188. package/dist/react/detail.d.ts +12 -6
  189. package/dist/react/detail.js +31 -24
  190. package/dist/react/detail.js.map +1 -1
  191. package/dist/react/discovery.js +1 -1
  192. package/dist/react/discovery.js.map +1 -1
  193. package/dist/react/folder.d.ts +7 -2
  194. package/dist/react/folder.js +165 -147
  195. package/dist/react/folder.js.map +1 -1
  196. package/dist/react/groups.d.ts +0 -1
  197. package/dist/react/groups.js +13 -12
  198. package/dist/react/groups.js.map +1 -1
  199. package/dist/react/index.d.ts +5 -3
  200. package/dist/react/location.d.ts +11 -0
  201. package/dist/react/location.js +18 -0
  202. package/dist/react/location.js.map +1 -0
  203. package/dist/react/public.d.ts +8 -0
  204. package/dist/react/public.js +32 -27
  205. package/dist/react/public.js.map +1 -1
  206. package/dist/react/resources.js +6 -0
  207. package/dist/react/resources.js.map +1 -1
  208. package/dist/react/stats.d.ts +24 -0
  209. package/dist/react/stats.js +28 -0
  210. package/dist/react/stats.js.map +1 -0
  211. package/dist/react/use-resource.js +8 -7
  212. package/dist/react/use-resource.js.map +1 -1
  213. package/dist/react/versions.js +20 -20
  214. package/dist/react.js +26 -24
  215. package/dist/styles.css +1 -1
  216. package/dist/urls/index.d.ts +35 -10
  217. package/dist/urls.js +100 -70
  218. package/dist/urls.js.map +1 -1
  219. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"zh.js","names":[],"sources":["../../src/locales/zh.ts"],"sourcesContent":["import type { ContentKitUiMessageBundle } from \"../i18n/messages.js\";\nimport { admin } from \"./admin/zh.js\";\nimport { social } from \"./social/zh.js\";\nimport { groups } from \"./groups/zh.js\";\nimport { discovery } from \"./discovery/zh.js\";\nimport { content } from \"./content/zh.js\";\n\nexport const zh: ContentKitUiMessageBundle = {\n ...social,\n ...admin,\n ...groups,\n ...discovery,\n ...content,\n common: {\n cancel: \"取消\",\n close: \"关闭\",\n save: \"保存\",\n saving: \"正在保存…\",\n retry: \"重试\",\n change: \"更换\",\n upload: \"上传\",\n editCrop: \"编辑裁剪\",\n remove: \"移除\",\n rename: \"重命名\",\n replace: \"替换\",\n },\n crop: {\n title: \"裁剪图片\",\n avatarTitle: \"裁剪头像\",\n coverTitle: \"裁剪封面\",\n description: \"拖动调整位置。滚动、双指缩放或使用滑块来缩放。\",\n area: \"裁剪区域。使用方向键移动图片,按 + 或 − 缩放,按 R 旋转。\",\n zoom: \"缩放\",\n zoomIn: \"放大\",\n zoomOut: \"缩小\",\n rotateLeft: \"向左旋转\",\n rotateRight: \"向右旋转\",\n reset: \"重置\",\n undersized: \"此裁剪宽 {width} 像素;在高分辨率屏幕上需要 {target} 像素或以上才清晰。\",\n tooSmall: \"此图片最宽 {width} 像素;至少需要 {min} 像素。请使用更大的图片。\",\n shape: \"比例\",\n original: \"原始比例\",\n preview: \"效果预览\",\n size: \"{width} × {height} 像素\",\n },\n sortable: {\n handle: \"调整 {name} 的顺序\",\n instructions: \"要调整顺序,请按空格键或回车键,用方向键移动,再按空格键或回车键放下。按 Esc 取消。\",\n pickedUp: \"已拿起 {name},位于第 {position} 位,共 {total} 项。\",\n over: \"{name} 将移到第 {position} 位,共 {total} 项。\",\n dropped: \"{name} 已放在第 {position} 位,共 {total} 项。\",\n droppedAway: \"{name} 已放下。\",\n canceled: \"已取消移动 {name}。\",\n },\n folder: {\n label: \"媒体\",\n drop: \"将文件拖放到此处,或点击选择\",\n add: \"添加文件\",\n commit: \"添加 {count} 个\",\n commitAgain: \"重新添加\",\n adding: \"正在添加…\",\n queued: \"等待中\",\n uploaded: \"已上传\",\n processing: \"处理中…\",\n processingPercent: \"处理中 {percent}%\",\n edited: \"已编辑\",\n failed: \"未处理\",\n failedVideo: \"无法播放\",\n crop: \"裁剪和旋转\",\n select: \"选择 {name}\",\n selected: \"已选择 {count} 个\",\n removeSelected: \"移除所选\",\n clearSelection: \"取消选择\",\n confirmRemove: \"要移除 {count} 个文件吗?此操作无法撤销。\",\n renameLabel: \"{name} 的新名称\",\n paused: \"上传已暂停\",\n resume: \"继续\",\n dismiss: \"关闭\",\n limitSize: \"{what}最大 {size}\",\n limitMax: \"最多 {max} 个\",\n limitAspect: \"视频比例为 {min} 至 {max}\",\n kinds: {\n image: \"图片\",\n video: \"视频\",\n audio: \"音频\",\n subtitle: \"字幕\",\n file: \"文件\",\n },\n },\n readiness: {\n processing: \"正在处理媒体\",\n processingHint: \"所有文件处理完成后将自动发布。\",\n failedOne: \"无法处理 {name}\",\n failedMany: \"无法处理 {count} 个文件:{names}\",\n failedHint: \"移除这些文件后将自动发布。\",\n },\n avatar: {\n label: \"头像\",\n empty: \"暂无头像\",\n hint: \"正方形图片,至少 {size} 像素。\",\n },\n cover: {\n label: \"封面图片\",\n empty: \"暂无封面\",\n hint: \"横向图片,至少 {width} × {height} 像素。\",\n drop: \"将图片拖放到此处\",\n },\n poster: {\n title: \"设置封面\",\n description: \"从视频中选取一帧,或上传一张图片。封面与视频比例相同。\",\n frame: \"视频画面\",\n upload: \"上传图片\",\n time: \"时间\",\n previousFrame: \"上一帧\",\n nextFrame: \"下一帧\",\n jumpTo: \"跳到 {time}\",\n useFrame: \"使用此帧\",\n crop: \"裁剪…\",\n cropTitle: \"裁剪封面\",\n auto: \"自动\",\n autoHint: \"在你选择之前,将使用自动选取的画面。\",\n drop: \"将图片拖到这里,或点击选择\",\n processing: \"视频仍在处理中。编码完成后即可选择封面。\",\n rendering: \"正在生成封面…\",\n },\n progress: {\n decoding: \"正在打开图片…\",\n hashing: \"正在准备…\",\n uploading: \"正在上传… {percent}%\",\n completing: \"正在完成上传…\",\n rendering: \"正在生成各尺寸…\",\n },\n encode: {\n processing: \"正在处理视频\",\n queued: \"等待处理\",\n queuedPosition: \"排队中 · 第 {position} 位\",\n stalled: \"处理已暂停,正在等待服务器…\",\n segments: \"片段 {done} / {total}\",\n higherQualities: \"更高画质\",\n remainingSeconds: \"剩余约 {seconds} 秒\",\n remainingMinutes: \"剩余约 {minutes} 分钟\",\n remainingHours: \"剩余约 {hours} 小时 {minutes} 分钟\",\n almostDone: \"即将完成\",\n estimating: \"正在估算…\",\n phase: {\n queued: \"排队中\",\n downloading: \"准备中\",\n probing: \"分析中\",\n encoding: \"编码中\",\n muxing: \"封装中\",\n uploading: \"保存中\",\n publishing: \"发布中\",\n images: \"正在生成封面\",\n },\n },\n gallery: {\n label: \"媒体\",\n view: \"布局\",\n carousel: \"轮播\",\n grid: \"网格\",\n counter: \"{current} / {total}\",\n slide: \"第 {current} 项,共 {total} 项\",\n previous: \"上一个\",\n next: \"下一个\",\n goTo: \"转到第 {index} 项\",\n open: \"打开第 {index} 项,共 {total} 项\",\n lightbox: \"媒体查看器\",\n lockedOne: \"还有 1 项已锁定\",\n lockedMany: \"还有 {count} 项已锁定\",\n processingImage: \"正在处理图片…\",\n image: \"图片 {index}\",\n video: \"视频 {index}\",\n audio: \"音频 {index}\",\n processingAudio: \"正在处理音频…\",\n download: \"下载\",\n failedAudio: \"无法处理此音频。\",\n failedImage: \"无法处理此图片。\",\n },\n player: {\n play: \"播放视频\",\n loading: \"正在加载视频…\",\n retry: \"重试\",\n code: \"代码 {code}\",\n quality: \"画质\",\n auto: \"自动\",\n autoCurrent: \"自动({quality})\",\n label: \"视频播放器\",\n resume: \"播放\",\n pause: \"暂停\",\n replay: \"重新播放\",\n seek: \"跳转\",\n time: \"{current} / {duration}\",\n seekBack: \"后退 {seconds} 秒\",\n seekForward: \"前进 {seconds} 秒\",\n seconds: \"{seconds} 秒\",\n mute: \"静音\",\n unmute: \"取消静音\",\n volume: \"音量\",\n settings: \"设置\",\n speed: \"播放速度\",\n normal: \"正常\",\n audio: \"音轨\",\n subtitles: \"字幕\",\n subtitlesOff: \"关闭\",\n forced: \"{label}(强制)\",\n track: \"轨道 {number}\",\n fullscreen: \"全屏\",\n exitFullscreen: \"退出全屏\",\n pip: \"画中画\",\n exitPip: \"退出画中画\",\n theater: \"影院模式\",\n miniPlayer: \"迷你播放器\",\n expand: \"展开\",\n close: \"关闭\",\n failed: \"无法处理此视频。\",\n errors: {\n network: \"无法连接到视频服务器。\",\n access: \"你已无权访问此视频。\",\n not_found: \"此视频不可用。\",\n rate_limited: \"请求过多,请稍后重试。\",\n unsupported: \"此浏览器无法播放此视频。\",\n stalled: \"播放已停滞。请检查网络连接后重试。\",\n unknown: \"目前无法播放此视频。\",\n },\n },\n errors: {\n generic: \"我们这边出了问题,请重试。\",\n network: \"连接已断开,请检查网络后重试。\",\n storage: \"上传中断,请重试。\",\n decode: \"无法将此文件作为图片打开。\",\n invalid_request: \"无法保存图片。\",\n unauthorized: \"会话已过期,请重新登录后上传。\",\n forbidden: \"你没有权限更改此图片。\",\n not_found: \"此项目已不存在。\",\n conflict: \"图片已在其他地方被更改,请刷新后重试。\",\n incomplete: \"上传未完成,请重试。\",\n not_uploaded: \"上传已过期,请重新选择图片。\",\n too_large: \"文件过大。\",\n quota_exceeded: \"已达到存储空间上限。\",\n type_not_allowed: \"此处不支持该文件类型。\",\n checksum_mismatch: \"上传过程中文件发生了变化,请重试。\",\n rate_limited: \"上传次数过多,请在 {seconds} 秒后重试。\",\n unavailable: \"媒体存储暂时不可用,请稍后重试。\",\n upgrade_required: \"此项目正在升级,请稍后重试。\",\n internal_error: \"我们这边出了问题,请重试。\",\n allowedTypes: \"请使用 {allowed}。\",\n tooLargeBy: \"此文件为 {size};上限为 {max}。\",\n image_too_small: \"裁剪后图片宽 {width} 像素;至少需要 {min} 像素。请选择更大的区域或使用更大的图片。\",\n image_too_large: \"此图片为 {width}×{height} 像素;图片最多 {megapixels} 百万像素。\",\n image_unreadable: \"此文件不是有效的 {format} 图片。\",\n render_timeout: \"服务器仍在处理,完成后会显示;请稍后再查看。\",\n animation_not_allowed: \"此处不允许动图。请上传静态图片。\",\n animation_unsupported: \"不支持动态 {format} 图片。请使用动态 WebP 或 GIF。\",\n animationFrames: \"动图最多 {maxFrames} 帧;此图有 {frames} 帧。\",\n animationSeconds: \"动图最长 {maxSeconds} 秒。\",\n animationTooLarge: \"此动图过大:{frames} 帧 {width}×{height} 像素。请减少帧数或缩小尺寸。\",\n too_many_files: \"此项目的文件数已达上限。请先删除一些。\",\n animation_too_long: \"此动图过长。\",\n video_too_long: \"此文件时长超出此处允许的范围。\",\n video_too_large: \"此视频的画面尺寸超出此处允许的范围。\",\n videoTooLongBy: \"此文件时长 {duration} 秒;此处上限为 {maxSeconds} 秒。\",\n videoTooLargeBy: \"此视频为 {width}×{height} 像素;视频最多 {megapixels} 百万像素。\",\n video_over_budget: \"处理此视频耗时过长。请使用更短或更小的视频。\",\n video_aspect_unsupported: \"此视频过宽或过高,无法在此使用。\",\n videoAspectBy: \"此视频的比例为 {shape};此处的视频比例须在 {minAspect} 至 {maxAspect} 之间。\",\n tooManyFilesBy: \"此项目最多可包含 {maxFiles} 个此类文件。请先移除一些。\",\n aborted: \"已取消。\",\n resume_mismatch: \"保存的上传属于另一个文件。请重新选择文件。\",\n moderation_rejected: \"已被审核拒绝。\",\n comment_banned: \"你无法在此评论。\",\n comments_closed: \"此处评论已关闭。\",\n limit_reached: \"已达到允许的上限。\",\n comment_too_long: \"评论太长了。\",\n commentTooLongBy: \"评论最多 {maxChars} 个字符。\",\n not_configured: \"此网站未提供该功能。\",\n tenant_mismatch: \"我们这边出了点问题,请重试。\",\n gone: \"此内容已不可用。\",\n },\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"zh.js","names":[],"sources":["../../src/locales/zh.ts"],"sourcesContent":["import type { ContentKitUiMessageBundle } from \"../i18n/messages.js\";\nimport { admin } from \"./admin/zh.js\";\nimport { social } from \"./social/zh.js\";\nimport { groups } from \"./groups/zh.js\";\nimport { discovery } from \"./discovery/zh.js\";\nimport { content } from \"./content/zh.js\";\n\nexport const zh: ContentKitUiMessageBundle = {\n ...social,\n ...admin,\n ...groups,\n ...discovery,\n ...content,\n common: {\n cancel: \"取消\",\n close: \"关闭\",\n save: \"保存\",\n saving: \"正在保存…\",\n retry: \"重试\",\n change: \"更换\",\n upload: \"上传\",\n editCrop: \"编辑裁剪\",\n remove: \"移除\",\n rename: \"重命名\",\n replace: \"替换\",\n },\n crop: {\n title: \"裁剪图片\",\n avatarTitle: \"裁剪头像\",\n coverTitle: \"裁剪封面\",\n description: \"拖动调整位置。滚动、双指缩放或使用滑块来缩放。\",\n area: \"裁剪区域。使用方向键移动图片,按 + 或 − 缩放,按 R 旋转。\",\n zoom: \"缩放\",\n zoomIn: \"放大\",\n zoomOut: \"缩小\",\n rotateLeft: \"向左旋转\",\n rotateRight: \"向右旋转\",\n reset: \"重置\",\n undersized: \"此裁剪宽 {width} 像素;在高分辨率屏幕上需要 {target} 像素或以上才清晰。\",\n tooSmall: \"此图片最宽 {width} 像素;至少需要 {min} 像素。请使用更大的图片。\",\n shape: \"比例\",\n original: \"原始比例\",\n preview: \"效果预览\",\n size: \"{width} × {height} 像素\",\n },\n sortable: {\n handle: \"调整 {name} 的顺序\",\n instructions: \"要调整顺序,请按空格键或回车键,用方向键移动,再按空格键或回车键放下。按 Esc 取消。\",\n pickedUp: \"已拿起 {name},位于第 {position} 位,共 {total} 项。\",\n over: \"{name} 将移到第 {position} 位,共 {total} 项。\",\n dropped: \"{name} 已放在第 {position} 位,共 {total} 项。\",\n droppedAway: \"{name} 已放下。\",\n canceled: \"已取消移动 {name}。\",\n },\n folder: {\n label: \"媒体\",\n drop: \"将文件拖放到此处,或点击选择\",\n add: \"添加文件\",\n commit: \"添加 {count} 个\",\n commitAgain: \"重新添加\",\n adding: \"正在添加…\",\n queued: \"等待中\",\n uploaded: \"已上传\",\n notAdded: \"已上传,尚未添加\",\n processing: \"处理中…\",\n processingPercent: \"处理中 {percent}%\",\n edited: \"已编辑\",\n failed: \"未处理\",\n failedVideo: \"无法播放\",\n crop: \"裁剪和旋转\",\n select: \"选择 {name}\",\n selected: \"已选择 {count} 个\",\n removeSelected: \"移除所选\",\n clearSelection: \"取消选择\",\n confirmRemove: \"要移除 {count} 个文件吗?此操作无法撤销。\",\n renameLabel: \"{name} 的新名称\",\n paused: \"上传已暂停\",\n resume: \"继续\",\n dismiss: \"关闭\",\n limitSize: \"{what}最大 {size}\",\n limitMax: \"最多 {max} 个\",\n limitAspect: \"视频比例为 {min} 至 {max}\",\n kinds: {\n image: \"图片\",\n video: \"视频\",\n audio: \"音频\",\n subtitle: \"字幕\",\n file: \"文件\",\n },\n },\n readiness: {\n processing: \"正在处理媒体\",\n processingHint: \"所有文件处理完成后将自动发布。\",\n failedOne: \"无法处理 {name}\",\n failedMany: \"无法处理 {count} 个文件:{names}\",\n failedHint: \"移除这些文件后将自动发布。\",\n },\n avatar: {\n label: \"头像\",\n empty: \"暂无头像\",\n hint: \"正方形图片,至少 {size} 像素。\",\n },\n cover: {\n label: \"封面图片\",\n empty: \"暂无封面\",\n hint: \"横向图片,至少 {width} × {height} 像素。\",\n drop: \"将图片拖放到此处\",\n },\n poster: {\n title: \"设置封面\",\n description: \"从视频中选取一帧,或上传一张图片。封面与视频比例相同。\",\n frame: \"视频画面\",\n upload: \"上传图片\",\n time: \"时间\",\n previousFrame: \"上一帧\",\n nextFrame: \"下一帧\",\n jumpTo: \"跳到 {time}\",\n useFrame: \"使用此帧\",\n crop: \"裁剪…\",\n cropTitle: \"裁剪封面\",\n auto: \"自动\",\n autoHint: \"在你选择之前,将使用自动选取的画面。\",\n drop: \"将图片拖到这里,或点击选择\",\n processing: \"视频仍在处理中。编码完成后即可选择封面。\",\n rendering: \"正在生成封面…\",\n },\n progress: {\n decoding: \"正在打开图片…\",\n hashing: \"正在准备…\",\n uploading: \"正在上传… {percent}%\",\n completing: \"正在完成上传…\",\n rendering: \"正在生成各尺寸…\",\n },\n encode: {\n processing: \"正在处理视频\",\n queued: \"等待处理\",\n queuedPosition: \"排队中 · 第 {position} 位\",\n stalled: \"处理已暂停,正在等待服务器…\",\n segments: \"片段 {done} / {total}\",\n higherQualities: \"更高画质\",\n remainingSeconds: \"剩余约 {seconds} 秒\",\n remainingMinutes: \"剩余约 {minutes} 分钟\",\n remainingHours: \"剩余约 {hours} 小时 {minutes} 分钟\",\n almostDone: \"即将完成\",\n estimating: \"正在估算…\",\n phase: {\n queued: \"排队中\",\n downloading: \"准备中\",\n probing: \"分析中\",\n encoding: \"编码中\",\n muxing: \"封装中\",\n uploading: \"保存中\",\n publishing: \"发布中\",\n images: \"正在生成封面\",\n },\n },\n gallery: {\n label: \"媒体\",\n view: \"布局\",\n carousel: \"轮播\",\n grid: \"网格\",\n counter: \"{current} / {total}\",\n slide: \"第 {current} 项,共 {total} 项\",\n previous: \"上一个\",\n next: \"下一个\",\n goTo: \"转到第 {index} 项\",\n open: \"打开第 {index} 项,共 {total} 项\",\n lightbox: \"媒体查看器\",\n lockedOne: \"还有 1 项已锁定\",\n lockedMany: \"还有 {count} 项已锁定\",\n processingImage: \"正在处理图片…\",\n image: \"图片 {index}\",\n video: \"视频 {index}\",\n audio: \"音频 {index}\",\n processingAudio: \"正在处理音频…\",\n download: \"下载\",\n failedAudio: \"无法处理此音频。\",\n failedImage: \"无法处理此图片。\",\n },\n player: {\n play: \"播放视频\",\n loading: \"正在加载视频…\",\n retry: \"重试\",\n code: \"代码 {code}\",\n quality: \"画质\",\n auto: \"自动\",\n autoCurrent: \"自动({quality})\",\n label: \"视频播放器\",\n resume: \"播放\",\n pause: \"暂停\",\n replay: \"重新播放\",\n seek: \"跳转\",\n time: \"{current} / {duration}\",\n seekBack: \"后退 {seconds} 秒\",\n seekForward: \"前进 {seconds} 秒\",\n seconds: \"{seconds} 秒\",\n mute: \"静音\",\n unmute: \"取消静音\",\n volume: \"音量\",\n settings: \"设置\",\n speed: \"播放速度\",\n normal: \"正常\",\n audio: \"音轨\",\n subtitles: \"字幕\",\n subtitlesOff: \"关闭\",\n forced: \"{label}(强制)\",\n track: \"轨道 {number}\",\n fullscreen: \"全屏\",\n exitFullscreen: \"退出全屏\",\n pip: \"画中画\",\n exitPip: \"退出画中画\",\n theater: \"影院模式\",\n miniPlayer: \"迷你播放器\",\n expand: \"展开\",\n close: \"关闭\",\n failed: \"无法处理此视频。\",\n errors: {\n network: \"无法连接到视频服务器。\",\n access: \"你已无权访问此视频。\",\n not_found: \"此视频不可用。\",\n rate_limited: \"请求过多,请稍后重试。\",\n unsupported: \"此浏览器无法播放此视频。\",\n stalled: \"播放已停滞。请检查网络连接后重试。\",\n unknown: \"目前无法播放此视频。\",\n },\n },\n errors: {\n generic: \"我们这边出了问题,请重试。\",\n network: \"连接已断开,请检查网络后重试。\",\n storage: \"上传中断,请重试。\",\n decode: \"无法将此文件作为图片打开。\",\n invalid_request: \"无法保存图片。\",\n unauthorized: \"会话已过期,请重新登录后上传。\",\n forbidden: \"你没有权限更改此图片。\",\n not_found: \"此项目已不存在。\",\n conflict: \"已在其他地方被更改,请刷新后重试。\",\n incomplete: \"上传未完成,请重试。\",\n not_uploaded: \"上传已过期,请重新选择图片。\",\n too_large: \"文件过大。\",\n quota_exceeded: \"已达到存储空间上限。\",\n type_not_allowed: \"此处不支持该文件类型。\",\n checksum_mismatch: \"上传过程中文件发生了变化,请重试。\",\n rate_limited: \"上传次数过多,请在 {seconds} 秒后重试。\",\n unavailable: \"媒体存储暂时不可用,请稍后重试。\",\n upgrade_required: \"此项目正在升级,请稍后重试。\",\n internal_error: \"我们这边出了问题,请重试。\",\n allowedTypes: \"请使用 {allowed}。\",\n tooLargeBy: \"此文件为 {size};上限为 {max}。\",\n image_too_small: \"裁剪后图片宽 {width} 像素;至少需要 {min} 像素。请选择更大的区域或使用更大的图片。\",\n image_too_large: \"此图片为 {width}×{height} 像素;图片最多 {megapixels} 百万像素。\",\n image_unreadable: \"此文件不是有效的 {format} 图片。\",\n render_timeout: \"服务器仍在处理,完成后会显示;请稍后再查看。\",\n animation_not_allowed: \"此处不允许动图。请上传静态图片。\",\n animation_unsupported: \"不支持动态 {format} 图片。请使用动态 WebP 或 GIF。\",\n animationFrames: \"动图最多 {maxFrames} 帧;此图有 {frames} 帧。\",\n animationSeconds: \"动图最长 {maxSeconds} 秒。\",\n animationTooLarge: \"此动图过大:{frames} 帧 {width}×{height} 像素。请减少帧数或缩小尺寸。\",\n too_many_files: \"此项目的文件数已达上限。请先删除一些。\",\n animation_too_long: \"此动图过长。\",\n video_too_long: \"此文件时长超出此处允许的范围。\",\n video_too_large: \"此视频的画面尺寸超出此处允许的范围。\",\n videoTooLongBy: \"此文件时长 {duration} 秒;此处上限为 {maxSeconds} 秒。\",\n videoTooLargeBy: \"此视频为 {width}×{height} 像素;视频最多 {megapixels} 百万像素。\",\n video_over_budget: \"处理此视频耗时过长。请使用更短或更小的视频。\",\n video_aspect_unsupported: \"此视频过宽或过高,无法在此使用。\",\n videoAspectBy: \"此视频的比例为 {shape};此处的视频比例须在 {minAspect} 至 {maxAspect} 之间。\",\n tooManyFilesBy: \"此项目最多可包含 {maxFiles} 个此类文件。请先移除一些。\",\n aborted: \"已取消。\",\n resume_mismatch: \"保存的上传属于另一个文件。请重新选择文件。\",\n moderation_rejected: \"已被审核拒绝。\",\n comment_banned: \"你无法在此评论。\",\n comments_closed: \"此处评论已关闭。\",\n comments_locked: \"解锁后即可查看和发表评论。\",\n not_joinable: \"目前无法加入此群组。\",\n limit_reached: \"已达到允许的上限。\",\n comment_too_long: \"评论太长了。\",\n commentTooLongBy: \"评论最多 {maxChars} 个字符。\",\n not_configured: \"此网站未提供该功能。\",\n tenant_mismatch: \"我们这边出了点问题,请重试。\",\n gone: \"此内容已不可用。\",\n },\n};\n"],"mappings":"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"}
@@ -39,7 +39,7 @@ export interface AdminLocation {
39
39
  */
40
40
  export declare function useAdminLocation(basePath: string | undefined, navigation: AdminNavigation | undefined): AdminLocation;
41
41
  /** A ContentKit staff area, each behind its own permission. */
42
- export type StaffArea = "comments" | "queue" | "bans" | "polls" | "contents" | "groups";
42
+ export type StaffArea = "comments" | "queue" | "bans" | "words" | "ips" | "polls" | "contents" | "groups";
43
43
  export interface StaffAccess<A extends StaffArea> {
44
44
  loading: boolean;
45
45
  /** Per area; true until known, and when the check itself failed (the area then shows its own error). */
@@ -2,10 +2,11 @@ import { toContentKitError as e } from "../client/errors.js";
2
2
  import { useContentKitClient as t } from "./context.js";
3
3
  import { keyOf as n, useContentScope as r, useResource as i } from "./use-resource.js";
4
4
  import { createContentKitClient as a } from "../client/client.js";
5
- import { useCallback as o, useMemo as s, useState as c, useSyncExternalStore as l } from "react";
5
+ import { MOVED as o, subscribeHistory as s } from "./location.js";
6
+ import { useCallback as c, useMemo as l, useState as u, useSyncExternalStore as d } from "react";
6
7
  //#region src/react/admin.ts
7
- function u({ client: e, baseUrl: n, auth: r }) {
8
- let i = s(() => e || n === void 0 ? null : a({
8
+ function f({ client: e, baseUrl: n, auth: r }) {
9
+ let i = l(() => e || n === void 0 ? null : a({
9
10
  baseUrl: n,
10
11
  fetch: r ? (e, t) => r.authFetch(e, t) : void 0,
11
12
  token: r?.getAccessToken ? () => r.getAccessToken() : void 0
@@ -16,9 +17,7 @@ function u({ client: e, baseUrl: n, auth: r }) {
16
17
  ]);
17
18
  return t(e ?? i);
18
19
  }
19
- var d = "ckui:navigate", f = (e) => (addEventListener("popstate", e), addEventListener(d, e), () => {
20
- removeEventListener("popstate", e), removeEventListener(d, e);
21
- }), p = () => location.pathname + location.search, m = () => "";
20
+ var p = () => location.pathname + location.search, m = () => "";
22
21
  function h(e = {}) {
23
22
  let t = new URLSearchParams();
24
23
  for (let [n, r] of Object.entries(e)) r != null && r !== "" && t.set(n, String(r));
@@ -26,31 +25,31 @@ function h(e = {}) {
26
25
  return n ? `?${n}` : "";
27
26
  }
28
27
  function g(e, t) {
29
- let n = l(t || e === void 0 ? _ : f, p, m), [r, i] = c({
28
+ let n = d(t || e === void 0 ? _ : s, p, m), [r, i] = u({
30
29
  view: "",
31
30
  search: ""
32
- }), a = e?.replace(/\/+$/, "") ?? "", u, g;
33
- if (e === void 0 && !t) ({view: u, search: g} = r);
31
+ }), a = e?.replace(/\/+$/, "") ?? "", f, g;
32
+ if (e === void 0 && !t) ({view: f, search: g} = r);
34
33
  else {
35
34
  let [e, r = ""] = v(t ? t.pathname + t.search : n, "?");
36
- g = r ? `?${r}` : "", u = e === a || e === `${a}/` ? "" : e.startsWith(`${a}/`) ? e.slice(a.length + 1).replace(/\/+$/, "") : "";
35
+ g = r ? `?${r}` : "", f = e === a || e === `${a}/` ? "" : e.startsWith(`${a}/`) ? e.slice(a.length + 1).replace(/\/+$/, "") : "";
37
36
  }
38
- let y = o((n, r, o = {}) => {
39
- let s = h(r);
37
+ let y = c((n, r, s = {}) => {
38
+ let c = h(r);
40
39
  if (e === void 0 && !t) return i({
41
40
  view: n,
42
- search: s
41
+ search: c
43
42
  });
44
- let c = (n ? `${a}/${n}` : a || "/") + s;
45
- if (t) return t.navigate(c, o);
46
- window.history[o.replace ? "replaceState" : "pushState"](null, "", c), dispatchEvent(new Event(d));
43
+ let l = (n ? `${a}/${n}` : a || "/") + c;
44
+ if (t) return t.navigate(l, s);
45
+ window.history[s.replace ? "replaceState" : "pushState"](null, "", l), dispatchEvent(new Event(o));
47
46
  }, [
48
47
  e,
49
48
  a,
50
49
  t
51
- ]), b = s(() => new URLSearchParams(g), [g]);
50
+ ]), b = l(() => new URLSearchParams(g), [g]);
52
51
  return {
53
- view: u,
52
+ view: f,
54
53
  query: b,
55
54
  go: y
56
55
  };
@@ -67,6 +66,8 @@ var y = "00000000-0000-7000-8000-000000000000", b = {
67
66
  limit: 1
68
67
  }, t),
69
68
  bans: (e, t) => e.bans.list("global", { limit: 1 }, t),
69
+ words: (e, t) => e.moderation.bannedWords(t),
70
+ ips: (e, t) => e.moderation.ipBans(t),
70
71
  polls: (e, t) => e.polls.adminList({ limit: 1 }, t),
71
72
  contents: (e, t) => e.contents.manage({ limit: 1 }, t),
72
73
  groups: (t, n) => t.groups.members(y, { limit: 1 }, n).catch((t) => {
@@ -74,7 +75,7 @@ var y = "00000000-0000-7000-8000-000000000000", b = {
74
75
  })
75
76
  };
76
77
  function x(t, a = {}) {
77
- let o = u(a), { store: s, scope: c } = r(o), l = i(s, n("access", c, ...t), { type: "access" }, async (n) => {
78
+ let o = f(a), { store: s, scope: c } = r(o), l = i(s, n("access", c, ...t), { type: "access" }, async (n) => {
78
79
  let r = {};
79
80
  return await Promise.all(t.map(async (t) => {
80
81
  try {
@@ -85,15 +86,15 @@ function x(t, a = {}) {
85
86
  r[t] = !1;
86
87
  }
87
88
  })), r;
88
- }), d = Object.fromEntries(t.map((e) => [e, l.data?.[e] ?? !0]));
89
+ }), u = Object.fromEntries(t.map((e) => [e, l.data?.[e] ?? !0]));
89
90
  return {
90
91
  loading: l.loading || !l.loaded,
91
- allowed: d,
92
- any: t.some((e) => d[e]),
92
+ allowed: u,
93
+ any: t.some((e) => u[e]),
93
94
  error: l.error
94
95
  };
95
96
  }
96
97
  //#endregion
97
- export { g as useAdminLocation, u as useFlowClient, x as useStaffAccess };
98
+ export { g as useAdminLocation, f as useFlowClient, x as useStaffAccess };
98
99
 
99
100
  //# sourceMappingURL=admin.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"admin.js","names":[],"sources":["../../src/react/admin.ts"],"sourcesContent":["import { useCallback, useMemo, useState, useSyncExternalStore } from \"react\";\nimport { createContentKitClient, type ContentKitClient } from \"../client/client.js\";\nimport { toContentKitError, type ContentKitError } from \"../client/errors.js\";\nimport type { TokenSource } from \"../client/http.js\";\nimport { useContentKitClient } from \"./context.js\";\nimport { keyOf, useContentScope, useResource } from \"./use-resource.js\";\n\n/** What a flow needs of the host's auth: an authenticating fetch, as auth-ui's AuthClient and IssuerClient have. */\nexport interface ContentKitAuth {\n authFetch: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;\n /** The access token, for HLS playlists on the API origin. */\n getAccessToken?: TokenSource;\n}\n\n/** How a flow reaches ContentKit: its own client, or baseUrl with the host's auth; else the provider's client. */\nexport interface FlowClientOptions {\n client?: ContentKitClient;\n baseUrl?: string;\n auth?: ContentKitAuth;\n}\n\n/** The flow's client: `client`, else one made from `baseUrl` and `auth`, else the provider's. */\nexport function useFlowClient({ client, baseUrl, auth }: FlowClientOptions): ContentKitClient {\n const made = useMemo(\n () =>\n client || baseUrl === undefined\n ? null\n : createContentKitClient({\n baseUrl,\n fetch: auth ? (input, init) => auth.authFetch(input, init) : undefined,\n token: auth?.getAccessToken ? () => auth.getAccessToken!() : undefined,\n }),\n [client, baseUrl, auth],\n );\n return useContentKitClient(client ?? made);\n}\n\n/** The host router as a flow sees it. With React Router: `{ pathname, search }` of useLocation() and useNavigate(). */\nexport interface AdminNavigation {\n pathname: string;\n search: string;\n navigate: (to: string, options?: { replace?: boolean }) => void;\n}\n\n/** A query for a view; empty values are left out. */\nexport type AdminQuery = Record<string, string | number | null | undefined>;\n\nexport interface AdminLocation {\n /** The view: the path under basePath, \"\" at its root. */\n view: string;\n query: URLSearchParams;\n go: (view: string, query?: AdminQuery, options?: { replace?: boolean }) => void;\n}\n\nconst MOVED = \"ckui:navigate\";\nconst subscribeHistory = (cb: () => void) => {\n addEventListener(\"popstate\", cb);\n addEventListener(MOVED, cb);\n return () => {\n removeEventListener(\"popstate\", cb);\n removeEventListener(MOVED, cb);\n };\n};\nconst historySnapshot = () => location.pathname + location.search;\nconst noHistory = () => \"\";\n\nfunction encode(query: AdminQuery = {}): string {\n const q = new URLSearchParams();\n for (const [k, v] of Object.entries(query)) if (v !== undefined && v !== null && v !== \"\") q.set(k, String(v));\n const s = q.toString();\n return s ? `?${s}` : \"\";\n}\n\n/**\n * A multi-view flow's place: under basePath in the host router (navigation),\n * else in the browser history; without basePath, in memory.\n */\nexport function useAdminLocation(basePath: string | undefined, navigation: AdminNavigation | undefined): AdminLocation {\n const history = useSyncExternalStore(navigation || basePath === undefined ? subscribeNone : subscribeHistory, historySnapshot, noHistory);\n const [memory, setMemory] = useState({ view: \"\", search: \"\" });\n const base = basePath?.replace(/\\/+$/, \"\") ?? \"\";\n let view: string;\n let search: string;\n if (basePath === undefined && !navigation) {\n ({ view, search } = memory);\n } else {\n const at = navigation ? navigation.pathname + navigation.search : history;\n const [pathname, query = \"\"] = splitOnce(at, \"?\");\n search = query ? `?${query}` : \"\";\n view = pathname === base || pathname === `${base}/` ? \"\" : pathname.startsWith(`${base}/`) ? pathname.slice(base.length + 1).replace(/\\/+$/, \"\") : \"\";\n }\n const go = useCallback(\n (to: string, query?: AdminQuery, o: { replace?: boolean } = {}) => {\n const qs = encode(query);\n if (basePath === undefined && !navigation) return setMemory({ view: to, search: qs });\n const url = (to ? `${base}/${to}` : base || \"/\") + qs;\n if (navigation) return navigation.navigate(url, o);\n window.history[o.replace ? \"replaceState\" : \"pushState\"](null, \"\", url);\n dispatchEvent(new Event(MOVED));\n },\n [basePath, base, navigation],\n );\n const query = useMemo(() => new URLSearchParams(search), [search]);\n return { view, query, go };\n}\n\nconst subscribeNone = () => () => {};\n\nfunction splitOnce(s: string, sep: string): [string, string?] {\n const i = s.indexOf(sep);\n return i < 0 ? [s] : [s.slice(0, i), s.slice(i + 1)];\n}\n\n/** A ContentKit staff area, each behind its own permission. */\nexport type StaffArea = \"comments\" | \"queue\" | \"bans\" | \"polls\" | \"contents\" | \"groups\";\n\n// No group has this id: staff get not_found, others a refusal.\nconst NO_GROUP = \"00000000-0000-7000-8000-000000000000\";\n\nconst PROBES: Record<StaffArea, (c: ContentKitClient, signal: AbortSignal) => Promise<unknown>> = {\n comments: (c, signal) => c.comments.adminList({ limit: 1 }, signal),\n queue: (c, signal) => c.moderation.held({ kind: \"comment\", limit: 1 }, signal),\n bans: (c, signal) => c.bans.list(\"global\", { limit: 1 }, signal),\n polls: (c, signal) => c.polls.adminList({ limit: 1 }, signal),\n contents: (c, signal) => c.contents.manage({ limit: 1 }, signal),\n groups: (c, signal) =>\n c.groups.members(NO_GROUP, { limit: 1 }, signal).catch((e) => {\n if (toContentKitError(e).code !== \"not_found\") throw e;\n }),\n};\n\nexport interface StaffAccess<A extends StaffArea> {\n loading: boolean;\n /** Per area; true until known, and when the check itself failed (the area then shows its own error). */\n allowed: Record<A, boolean>;\n /** Some area is allowed. */\n any: boolean;\n error?: ContentKitError;\n}\n\n/**\n * What the caller may do in each area: one small read per area, refused\n * (401, 403) or not, shared by every hook asking on the client.\n */\nexport function useStaffAccess<A extends StaffArea>(areas: readonly A[], o: FlowClientOptions = {}): StaffAccess<A> {\n const client = useFlowClient(o);\n const { store, scope } = useContentScope(client);\n const r = useResource<Record<string, boolean>>(store, keyOf(\"access\", scope, ...areas), { type: \"access\" }, async (signal) => {\n const out: Record<string, boolean> = {};\n await Promise.all(\n areas.map(async (a) => {\n try {\n await PROBES[a](client, signal);\n out[a] = true;\n } catch (e) {\n const err = toContentKitError(e);\n if (err.code !== \"forbidden\" && err.code !== \"unauthorized\") throw err;\n out[a] = false;\n }\n }),\n );\n return out;\n });\n const allowed = Object.fromEntries(areas.map((a) => [a, r.data?.[a] ?? true])) as Record<A, boolean>;\n return { loading: r.loading || !r.loaded, allowed, any: areas.some((a) => allowed[a]), error: r.error };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"admin.js","names":[],"sources":["../../src/react/admin.ts"],"sourcesContent":["import { useCallback, useMemo, useState, useSyncExternalStore } from \"react\";\nimport { createContentKitClient, type ContentKitClient } from \"../client/client.js\";\nimport { toContentKitError, type ContentKitError } from \"../client/errors.js\";\nimport type { TokenSource } from \"../client/http.js\";\nimport { useContentKitClient } from \"./context.js\";\nimport { MOVED, subscribeHistory } from \"./location.js\";\nimport { keyOf, useContentScope, useResource } from \"./use-resource.js\";\n\n/** What a flow needs of the host's auth: an authenticating fetch, as auth-ui's AuthClient and IssuerClient have. */\nexport interface ContentKitAuth {\n authFetch: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;\n /** The access token, for HLS playlists on the API origin. */\n getAccessToken?: TokenSource;\n}\n\n/** How a flow reaches ContentKit: its own client, or baseUrl with the host's auth; else the provider's client. */\nexport interface FlowClientOptions {\n client?: ContentKitClient;\n baseUrl?: string;\n auth?: ContentKitAuth;\n}\n\n/** The flow's client: `client`, else one made from `baseUrl` and `auth`, else the provider's. */\nexport function useFlowClient({ client, baseUrl, auth }: FlowClientOptions): ContentKitClient {\n const made = useMemo(\n () =>\n client || baseUrl === undefined\n ? null\n : createContentKitClient({\n baseUrl,\n fetch: auth ? (input, init) => auth.authFetch(input, init) : undefined,\n token: auth?.getAccessToken ? () => auth.getAccessToken!() : undefined,\n }),\n [client, baseUrl, auth],\n );\n return useContentKitClient(client ?? made);\n}\n\n/** The host router as a flow sees it. With React Router: `{ pathname, search }` of useLocation() and useNavigate(). */\nexport interface AdminNavigation {\n pathname: string;\n search: string;\n navigate: (to: string, options?: { replace?: boolean }) => void;\n}\n\n/** A query for a view; empty values are left out. */\nexport type AdminQuery = Record<string, string | number | null | undefined>;\n\nexport interface AdminLocation {\n /** The view: the path under basePath, \"\" at its root. */\n view: string;\n query: URLSearchParams;\n go: (view: string, query?: AdminQuery, options?: { replace?: boolean }) => void;\n}\n\nconst historySnapshot = () => location.pathname + location.search;\nconst noHistory = () => \"\";\n\nfunction encode(query: AdminQuery = {}): string {\n const q = new URLSearchParams();\n for (const [k, v] of Object.entries(query)) if (v !== undefined && v !== null && v !== \"\") q.set(k, String(v));\n const s = q.toString();\n return s ? `?${s}` : \"\";\n}\n\n/**\n * A multi-view flow's place: under basePath in the host router (navigation),\n * else in the browser history; without basePath, in memory.\n */\nexport function useAdminLocation(basePath: string | undefined, navigation: AdminNavigation | undefined): AdminLocation {\n const history = useSyncExternalStore(navigation || basePath === undefined ? subscribeNone : subscribeHistory, historySnapshot, noHistory);\n const [memory, setMemory] = useState({ view: \"\", search: \"\" });\n const base = basePath?.replace(/\\/+$/, \"\") ?? \"\";\n let view: string;\n let search: string;\n if (basePath === undefined && !navigation) {\n ({ view, search } = memory);\n } else {\n const at = navigation ? navigation.pathname + navigation.search : history;\n const [pathname, query = \"\"] = splitOnce(at, \"?\");\n search = query ? `?${query}` : \"\";\n view = pathname === base || pathname === `${base}/` ? \"\" : pathname.startsWith(`${base}/`) ? pathname.slice(base.length + 1).replace(/\\/+$/, \"\") : \"\";\n }\n const go = useCallback(\n (to: string, query?: AdminQuery, o: { replace?: boolean } = {}) => {\n const qs = encode(query);\n if (basePath === undefined && !navigation) return setMemory({ view: to, search: qs });\n const url = (to ? `${base}/${to}` : base || \"/\") + qs;\n if (navigation) return navigation.navigate(url, o);\n window.history[o.replace ? \"replaceState\" : \"pushState\"](null, \"\", url);\n dispatchEvent(new Event(MOVED));\n },\n [basePath, base, navigation],\n );\n const query = useMemo(() => new URLSearchParams(search), [search]);\n return { view, query, go };\n}\n\nconst subscribeNone = () => () => {};\n\nfunction splitOnce(s: string, sep: string): [string, string?] {\n const i = s.indexOf(sep);\n return i < 0 ? [s] : [s.slice(0, i), s.slice(i + 1)];\n}\n\n/** A ContentKit staff area, each behind its own permission. */\nexport type StaffArea = \"comments\" | \"queue\" | \"bans\" | \"words\" | \"ips\" | \"polls\" | \"contents\" | \"groups\";\n\n// No group has this id: staff get not_found, others a refusal.\nconst NO_GROUP = \"00000000-0000-7000-8000-000000000000\";\n\nconst PROBES: Record<StaffArea, (c: ContentKitClient, signal: AbortSignal) => Promise<unknown>> = {\n comments: (c, signal) => c.comments.adminList({ limit: 1 }, signal),\n queue: (c, signal) => c.moderation.held({ kind: \"comment\", limit: 1 }, signal),\n bans: (c, signal) => c.bans.list(\"global\", { limit: 1 }, signal),\n words: (c, signal) => c.moderation.bannedWords(signal),\n ips: (c, signal) => c.moderation.ipBans(signal),\n polls: (c, signal) => c.polls.adminList({ limit: 1 }, signal),\n contents: (c, signal) => c.contents.manage({ limit: 1 }, signal),\n groups: (c, signal) =>\n c.groups.members(NO_GROUP, { limit: 1 }, signal).catch((e) => {\n if (toContentKitError(e).code !== \"not_found\") throw e;\n }),\n};\n\nexport interface StaffAccess<A extends StaffArea> {\n loading: boolean;\n /** Per area; true until known, and when the check itself failed (the area then shows its own error). */\n allowed: Record<A, boolean>;\n /** Some area is allowed. */\n any: boolean;\n error?: ContentKitError;\n}\n\n/**\n * What the caller may do in each area: one small read per area, refused\n * (401, 403) or not, shared by every hook asking on the client.\n */\nexport function useStaffAccess<A extends StaffArea>(areas: readonly A[], o: FlowClientOptions = {}): StaffAccess<A> {\n const client = useFlowClient(o);\n const { store, scope } = useContentScope(client);\n const r = useResource<Record<string, boolean>>(store, keyOf(\"access\", scope, ...areas), { type: \"access\" }, async (signal) => {\n const out: Record<string, boolean> = {};\n await Promise.all(\n areas.map(async (a) => {\n try {\n await PROBES[a](client, signal);\n out[a] = true;\n } catch (e) {\n const err = toContentKitError(e);\n if (err.code !== \"forbidden\" && err.code !== \"unauthorized\") throw err;\n out[a] = false;\n }\n }),\n );\n return out;\n });\n const allowed = Object.fromEntries(areas.map((a) => [a, r.data?.[a] ?? true])) as Record<A, boolean>;\n return { loading: r.loading || !r.loaded, allowed, any: areas.some((a) => allowed[a]), error: r.error };\n}\n"],"mappings":"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"}
@@ -15,7 +15,7 @@ export interface CanonicalContentOptions {
15
15
  defaultLanguage?: string;
16
16
  }
17
17
  export interface CanonicalContent {
18
- /** The canonical path in the page's language; undefined until link arrives. */
18
+ /** The canonical path in the page's language, never a version's (?v=); undefined until link arrives. */
19
19
  path?: string;
20
20
  /** Its absolute URL. */
21
21
  url?: string;
@@ -24,8 +24,9 @@ export interface CanonicalContent {
24
24
  }
25
25
  /**
26
26
  * A content page's canonical URL once its data (link) arrives: replaces the
27
- * address with the canonical path (code spelling, merged code, current slug)
28
- * through the provider's navigate (else history.replaceState), and keeps
27
+ * address with the canonical path (language prefix, the reader's when the
28
+ * address has none; code spelling, merged code, current slug; the query, ?v=
29
+ * included, kept) through the provider's navigate (else history.replaceState), and keeps
29
30
  * `<link rel=canonical>`, og:url, og:title, og:image and hreflang alternates
30
31
  * (each language's own slug) in the head while mounted; unmounting removes
31
32
  * the tags it set, server-rendered ones included. Needs the provider's `urls`.
@@ -20,56 +20,57 @@ var s = (e, t, n) => ({
20
20
  value: t
21
21
  });
22
22
  function c(c, l = {}) {
23
- let u = t(), d = n(e)?.navigate, f = l.location ?? (typeof location > "u" ? "" : location.pathname + location.search + location.hash), p = c?.content_kind, m = c?.code, h = c?.slug, g = JSON.stringify(c?.slugs ?? {}), _ = l.languages?.join(","), v = i(() => p && m ? {
24
- content_kind: p,
25
- code: m,
26
- slug: h ?? "",
27
- slugs: JSON.parse(g)
23
+ let u = t(), d = n(e), f = d?.navigate, p = d?.language, m = l.location ?? (typeof location > "u" ? "" : location.pathname + location.search + location.hash), h = c?.content_kind, g = c?.code, _ = c?.slug, v = JSON.stringify(c?.slugs ?? {}), y = l.languages?.join(","), b = i(() => h && g ? {
24
+ content_kind: h,
25
+ code: g,
26
+ slug: _ ?? "",
27
+ slugs: JSON.parse(v)
28
28
  } : null, [
29
- p,
30
- m,
31
29
  h,
32
- g
33
- ]), y = i(() => {
34
- if (!v) return { alternates: {} };
35
- let e = u.origin ?? (typeof location > "u" ? void 0 : location.origin), t = f.includes("#") ? f.slice(f.indexOf("#")) : "", n = u.canonical(f, v), r = n?.path ?? u.path(v), i = e ? u.hreflang(v, {
30
+ g,
31
+ _,
32
+ v
33
+ ]), x = i(() => {
34
+ if (!b) return { alternates: {} };
35
+ let e = u.origin ?? (typeof location > "u" ? void 0 : location.origin), t = m.includes("#") ? m.slice(m.indexOf("#")) : "", n = p && u.languages.includes(p) ? p : void 0, r = u.canonical(m, b, { language: n }), i = r?.path ?? u.path(b, { language: n }), a = e ? u.hreflang(b, {
36
36
  origin: e,
37
- languages: _?.split(","),
37
+ languages: y?.split(","),
38
38
  defaultLanguage: l.defaultLanguage
39
39
  }) : {};
40
40
  return {
41
- path: r,
42
- url: e ? e + r : void 0,
43
- alternates: i,
44
- redirect: n?.redirect ? n.location + t : void 0
41
+ path: i,
42
+ url: e ? e + i : void 0,
43
+ alternates: a,
44
+ redirect: r?.redirect ? r.location + t : void 0
45
45
  };
46
46
  }, [
47
- v,
48
- f,
47
+ b,
48
+ m,
49
49
  u,
50
- _,
50
+ p,
51
+ y,
51
52
  l.defaultLanguage
52
- ]), { redirect: b } = y;
53
+ ]), { redirect: S } = x;
53
54
  r(() => {
54
- b && (d ? d(b, { replace: !0 }) : history.replaceState(history.state, "", b));
55
- }, [b, d]);
56
- let { url: x, alternates: S } = y, { title: C, image: w } = l, T = JSON.stringify(S);
55
+ S && (f ? f(S, { replace: !0 }) : history.replaceState(history.state, "", S));
56
+ }, [S, f]);
57
+ let { url: C, alternates: w } = x, { title: T, image: E } = l, D = JSON.stringify(w);
57
58
  return r(() => {
58
- if (!x || typeof document > "u") return;
59
- let e = [s("canonical", x), a("og:url", x)];
60
- C && e.push(a("og:title", C)), w && e.push(a("og:image", w));
61
- for (let [t, n] of Object.entries(JSON.parse(T))) e.push(s("alternate", n, t));
59
+ if (!C || typeof document > "u") return;
60
+ let e = [s("canonical", C), a("og:url", C)];
61
+ T && e.push(a("og:title", T)), E && e.push(a("og:image", E));
62
+ for (let [t, n] of Object.entries(JSON.parse(D))) e.push(s("alternate", n, t));
62
63
  let t = e.map((e) => o(document.head.querySelector(e.selector) ?? document.head.appendChild(e.create()), e.attr, e.value));
63
64
  return () => t.forEach((e) => e.remove());
64
65
  }, [
65
- x,
66
66
  C,
67
- w,
68
- T
67
+ T,
68
+ E,
69
+ D
69
70
  ]), {
70
- path: y.path,
71
- url: y.url,
72
- alternates: y.alternates
71
+ path: x.path,
72
+ url: x.url,
73
+ alternates: x.alternates
73
74
  };
74
75
  }
75
76
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"canonical.js","names":[],"sources":["../../src/react/canonical.ts"],"sourcesContent":["import { useContext, useEffect, useMemo } from \"react\";\nimport type { ContentLink } from \"../urls/index.js\";\nimport { ContentKitContext, useContentURLs } from \"./context.js\";\n\nexport interface CanonicalContentOptions {\n /** og:title. */\n title?: string;\n /** og:image, an absolute URL. */\n image?: string;\n /**\n * The current location (pathname, search, hash). Default\n * window.location; pass the router's so a navigation checks again.\n */\n location?: string;\n /** Languages the content exists in, for hreflang; default every configured language. */\n languages?: readonly string[];\n /** The language hreflang x-default points at. */\n defaultLanguage?: string;\n}\n\nexport interface CanonicalContent {\n /** The canonical path in the page's language; undefined until link arrives. */\n path?: string;\n /** Its absolute URL. */\n url?: string;\n /** hreflang alternates by language (and x-default). */\n alternates: Record<string, string>;\n}\n\ntype Tag = { selector: string; create: () => HTMLElement; attr: string; value: string };\n\nconst meta = (property: string, value: string): Tag => ({\n selector: `meta[property=\"${property}\"]`,\n create: () => withAttr(document.createElement(\"meta\"), \"property\", property),\n attr: \"content\",\n value,\n});\n\nfunction withAttr<E extends HTMLElement>(el: E, name: string, value: string): E {\n el.setAttribute(name, value);\n return el;\n}\n\nconst link = (rel: string, value: string, hreflang?: string): Tag => ({\n selector: hreflang ? `link[rel=\"${rel}\"][hreflang=\"${hreflang}\"]` : `link[rel=\"${rel}\"]:not([hreflang])`,\n create: () => {\n const el = withAttr(document.createElement(\"link\"), \"rel\", rel);\n if (hreflang) el.setAttribute(\"hreflang\", hreflang);\n return el;\n },\n attr: \"href\",\n value,\n});\n\n/**\n * A content page's canonical URL once its data (link) arrives: replaces the\n * address with the canonical path (code spelling, merged code, current slug)\n * through the provider's navigate (else history.replaceState), and keeps\n * `<link rel=canonical>`, og:url, og:title, og:image and hreflang alternates\n * (each language's own slug) in the head while mounted; unmounting removes\n * the tags it set, server-rendered ones included. Needs the provider's `urls`.\n */\nexport function useCanonicalContent(content: ContentLink | null | undefined, o: CanonicalContentOptions = {}): CanonicalContent {\n const urls = useContentURLs();\n const navigate = useContext(ContentKitContext)?.navigate;\n const at = o.location ?? (typeof location === \"undefined\" ? \"\" : location.pathname + location.search + location.hash);\n const kind = content?.content_kind;\n const code = content?.code;\n const slug = content?.slug;\n const slugs = JSON.stringify(content?.slugs ?? {});\n const languages = o.languages?.join(\",\");\n const lnk = useMemo(\n () => (kind && code ? ({ content_kind: kind, code, slug: slug ?? \"\", slugs: JSON.parse(slugs) as Record<string, string> } satisfies ContentLink) : null),\n [kind, code, slug, slugs],\n );\n\n const result = useMemo((): CanonicalContent & { redirect?: string } => {\n if (!lnk) return { alternates: {} };\n const origin = urls.origin ?? (typeof location === \"undefined\" ? undefined : location.origin);\n const hash = at.includes(\"#\") ? at.slice(at.indexOf(\"#\")) : \"\";\n const c = urls.canonical(at, lnk);\n const path = c?.path ?? urls.path(lnk);\n const alternates = origin ? urls.hreflang(lnk, { origin, languages: languages?.split(\",\"), defaultLanguage: o.defaultLanguage }) : {};\n return { path, url: origin ? origin + path : undefined, alternates, redirect: c?.redirect ? c.location + hash : undefined };\n }, [lnk, at, urls, languages, o.defaultLanguage]);\n\n const { redirect } = result;\n useEffect(() => {\n if (!redirect) return;\n if (navigate) navigate(redirect, { replace: true });\n else history.replaceState(history.state, \"\", redirect);\n }, [redirect, navigate]);\n\n const { url, alternates } = result;\n const { title, image } = o;\n const alternatesKey = JSON.stringify(alternates);\n useEffect(() => {\n if (!url || typeof document === \"undefined\") return;\n const tags: Tag[] = [link(\"canonical\", url), meta(\"og:url\", url)];\n if (title) tags.push(meta(\"og:title\", title));\n if (image) tags.push(meta(\"og:image\", image));\n for (const [lang, href] of Object.entries(JSON.parse(alternatesKey) as Record<string, string>)) tags.push(link(\"alternate\", href, lang));\n const set = tags.map((t) => withAttr(document.head.querySelector<HTMLElement>(t.selector) ?? document.head.appendChild(t.create()), t.attr, t.value));\n // The tags describe this page: leaving it removes them, server-rendered ones\n // included (restoring those would leave this page's values on the next).\n return () => set.forEach((el) => el.remove());\n }, [url, title, image, alternatesKey]);\n\n return { path: result.path, url: result.url, alternates: result.alternates };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"canonical.js","names":[],"sources":["../../src/react/canonical.ts"],"sourcesContent":["import { useContext, useEffect, useMemo } from \"react\";\nimport type { ContentLink } from \"../urls/index.js\";\nimport { ContentKitContext, useContentURLs } from \"./context.js\";\n\nexport interface CanonicalContentOptions {\n /** og:title. */\n title?: string;\n /** og:image, an absolute URL. */\n image?: string;\n /**\n * The current location (pathname, search, hash). Default\n * window.location; pass the router's so a navigation checks again.\n */\n location?: string;\n /** Languages the content exists in, for hreflang; default every configured language. */\n languages?: readonly string[];\n /** The language hreflang x-default points at. */\n defaultLanguage?: string;\n}\n\nexport interface CanonicalContent {\n /** The canonical path in the page's language, never a version's (?v=); undefined until link arrives. */\n path?: string;\n /** Its absolute URL. */\n url?: string;\n /** hreflang alternates by language (and x-default). */\n alternates: Record<string, string>;\n}\n\ntype Tag = { selector: string; create: () => HTMLElement; attr: string; value: string };\n\nconst meta = (property: string, value: string): Tag => ({\n selector: `meta[property=\"${property}\"]`,\n create: () => withAttr(document.createElement(\"meta\"), \"property\", property),\n attr: \"content\",\n value,\n});\n\nfunction withAttr<E extends HTMLElement>(el: E, name: string, value: string): E {\n el.setAttribute(name, value);\n return el;\n}\n\nconst link = (rel: string, value: string, hreflang?: string): Tag => ({\n selector: hreflang ? `link[rel=\"${rel}\"][hreflang=\"${hreflang}\"]` : `link[rel=\"${rel}\"]:not([hreflang])`,\n create: () => {\n const el = withAttr(document.createElement(\"link\"), \"rel\", rel);\n if (hreflang) el.setAttribute(\"hreflang\", hreflang);\n return el;\n },\n attr: \"href\",\n value,\n});\n\n/**\n * A content page's canonical URL once its data (link) arrives: replaces the\n * address with the canonical path (language prefix, the reader's when the\n * address has none; code spelling, merged code, current slug; the query, ?v=\n * included, kept) through the provider's navigate (else history.replaceState), and keeps\n * `<link rel=canonical>`, og:url, og:title, og:image and hreflang alternates\n * (each language's own slug) in the head while mounted; unmounting removes\n * the tags it set, server-rendered ones included. Needs the provider's `urls`.\n */\nexport function useCanonicalContent(content: ContentLink | null | undefined, o: CanonicalContentOptions = {}): CanonicalContent {\n const urls = useContentURLs();\n const ctx = useContext(ContentKitContext);\n const navigate = ctx?.navigate;\n const reader = ctx?.language;\n const at = o.location ?? (typeof location === \"undefined\" ? \"\" : location.pathname + location.search + location.hash);\n const kind = content?.content_kind;\n const code = content?.code;\n const slug = content?.slug;\n const slugs = JSON.stringify(content?.slugs ?? {});\n const languages = o.languages?.join(\",\");\n const lnk = useMemo(\n () => (kind && code ? ({ content_kind: kind, code, slug: slug ?? \"\", slugs: JSON.parse(slugs) as Record<string, string> } satisfies ContentLink) : null),\n [kind, code, slug, slugs],\n );\n\n const result = useMemo((): CanonicalContent & { redirect?: string } => {\n if (!lnk) return { alternates: {} };\n const origin = urls.origin ?? (typeof location === \"undefined\" ? undefined : location.origin);\n const hash = at.includes(\"#\") ? at.slice(at.indexOf(\"#\")) : \"\";\n const language = reader && urls.languages.includes(reader) ? reader : undefined;\n const c = urls.canonical(at, lnk, { language });\n const path = c?.path ?? urls.path(lnk, { language });\n const alternates = origin ? urls.hreflang(lnk, { origin, languages: languages?.split(\",\"), defaultLanguage: o.defaultLanguage }) : {};\n return { path, url: origin ? origin + path : undefined, alternates, redirect: c?.redirect ? c.location + hash : undefined };\n }, [lnk, at, urls, reader, languages, o.defaultLanguage]);\n\n const { redirect } = result;\n useEffect(() => {\n if (!redirect) return;\n if (navigate) navigate(redirect, { replace: true });\n else history.replaceState(history.state, \"\", redirect);\n }, [redirect, navigate]);\n\n const { url, alternates } = result;\n const { title, image } = o;\n const alternatesKey = JSON.stringify(alternates);\n useEffect(() => {\n if (!url || typeof document === \"undefined\") return;\n const tags: Tag[] = [link(\"canonical\", url), meta(\"og:url\", url)];\n if (title) tags.push(meta(\"og:title\", title));\n if (image) tags.push(meta(\"og:image\", image));\n for (const [lang, href] of Object.entries(JSON.parse(alternatesKey) as Record<string, string>)) tags.push(link(\"alternate\", href, lang));\n const set = tags.map((t) => withAttr(document.head.querySelector<HTMLElement>(t.selector) ?? document.head.appendChild(t.create()), t.attr, t.value));\n // The tags describe this page: leaving it removes them, server-rendered ones\n // included (restoring those would leave this page's values on the next).\n return () => set.forEach((el) => el.remove());\n }, [url, title, image, alternatesKey]);\n\n return { path: result.path, url: result.url, alternates: result.alternates };\n}\n"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  import { ContentKitClient } from '../client/client.js';
2
2
  import { BanScope, Decision, HeldKind, Reaction, Sort, Target } from '../client/content/types.js';
3
3
  import { ContentKitError } from '../client/errors.js';
4
- import { AdminComment, BanInput, Comment, CommentBan, CommentStanding, FeedItem, HeldItem, ReactionCounts } from '../client/generated/wire.js';
4
+ import { AdminComment, BanInput, BannedWord, BannedWordInput, Comment, CommentBan, CommentStanding, FeedItem, HeldItem, IPBan, IPBanInput, ReactionCounts } from '../client/generated/wire.js';
5
5
  import { UseList } from './use-resource.js';
6
6
  /** Counts after the caller's reaction changes from counts.mine to value. */
7
7
  export declare function withReaction(counts: ReactionCounts, value: Reaction): ReactionCounts;
@@ -84,6 +84,34 @@ export declare function useCommentBans(o?: {
84
84
  enabled?: boolean;
85
85
  client?: ContentKitClient;
86
86
  }): UseCommentBans;
87
+ export interface UseBannedWords {
88
+ words: BannedWord[];
89
+ loading: boolean;
90
+ error?: ContentKitError;
91
+ reload: () => void;
92
+ /** Bans words, or changes what banned ones do. */
93
+ ban: (words: BannedWordInput[]) => Promise<BannedWord[]>;
94
+ unban: (words: string[]) => Promise<void>;
95
+ }
96
+ /** Staff: the banned words every comment and answer meets (root:comments:moderate). */
97
+ export declare function useBannedWords(o?: {
98
+ enabled?: boolean;
99
+ client?: ContentKitClient;
100
+ }): UseBannedWords;
101
+ export interface UseIPBans {
102
+ bans: IPBan[];
103
+ loading: boolean;
104
+ error?: ContentKitError;
105
+ reload: () => void;
106
+ /** Bans addresses or networks, or replaces their bans. */
107
+ ban: (bans: IPBanInput[]) => Promise<IPBan[]>;
108
+ lift: (networks: string[]) => Promise<void>;
109
+ }
110
+ /** Staff: the IP bans on commenting and answering, newest first (root:comments:moderate). */
111
+ export declare function useIPBans(o?: {
112
+ enabled?: boolean;
113
+ client?: ContentKitClient;
114
+ }): UseIPBans;
87
115
  export interface UseGlobalCommentBan {
88
116
  /** The user's site-wide ban, expired or not; null when none. */
89
117
  ban: CommentBan | null;
@@ -165,7 +165,35 @@ function g(i = {}) {
165
165
  lift: p
166
166
  };
167
167
  }
168
- function _(t, r = {}) {
168
+ function _(t = {}) {
169
+ let { client: r, store: o, scope: s } = n(t.client), c = i(o, t.enabled === !1 ? null : e("moderation-rules", s, "words"), {
170
+ type: "moderation-rules",
171
+ scope: "words"
172
+ }, (e) => r.moderation.bannedWords(e)), l = a((e) => r.moderation.banWords(e), [r]), u = a((e) => r.moderation.unbanWords(e), [r]);
173
+ return {
174
+ words: c.data ?? [],
175
+ loading: t.enabled !== !1 && (c.loading || !c.loaded),
176
+ error: c.error,
177
+ reload: c.reload,
178
+ ban: l,
179
+ unban: u
180
+ };
181
+ }
182
+ function v(t = {}) {
183
+ let { client: r, store: o, scope: s } = n(t.client), c = i(o, t.enabled === !1 ? null : e("moderation-rules", s, "ips"), {
184
+ type: "moderation-rules",
185
+ scope: "ips"
186
+ }, (e) => r.moderation.ipBans(e)), l = a((e) => r.moderation.banIPs(e), [r]), u = a((e) => r.moderation.liftIPBans(e), [r]);
187
+ return {
188
+ bans: c.data ?? [],
189
+ loading: t.enabled !== !1 && (c.loading || !c.loaded),
190
+ error: c.error,
191
+ reload: c.reload,
192
+ ban: l,
193
+ lift: u
194
+ };
195
+ }
196
+ function y(t, r = {}) {
169
197
  let { client: o, store: s, scope: c } = n(r.client), l = i(s, e("ban", c, t), {
170
198
  type: "bans",
171
199
  scope: "global",
@@ -181,6 +209,6 @@ function _(t, r = {}) {
181
209
  };
182
210
  }
183
211
  //#endregion
184
- export { m as useAdminComments, f as useCanComment, g as useCommentBans, d as useCommentReplies, u as useComments, _ as useGlobalCommentBan, p as useLatestComments, h as useModerationQueue, o as withReaction };
212
+ export { m as useAdminComments, _ as useBannedWords, f as useCanComment, g as useCommentBans, d as useCommentReplies, u as useComments, y as useGlobalCommentBan, v as useIPBans, p as useLatestComments, h as useModerationQueue, o as withReaction };
185
213
 
186
214
  //# sourceMappingURL=comments.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"comments.js","names":[],"sources":["../../src/react/comments.ts"],"sourcesContent":["import { useCallback } from \"react\";\nimport type { ContentKitClient } from \"../client/client.js\";\nimport type { BanScope, Decision, HeldKind, Reaction, Sort, Target } from \"../client/content/types.js\";\nimport type { ContentKitError } from \"../client/errors.js\";\nimport type { AdminComment, BanInput, Comment, CommentBan, CommentStanding, FeedItem, HeldItem, ReactionCounts } from \"../client/generated/wire.js\";\nimport type { Page, ResourceStore } from \"./resources.js\";\nimport { keyOf, offsetPages, useContentScope, useList, useResource, type UseList } from \"./use-resource.js\";\n\n/** Counts after the caller's reaction changes from counts.mine to value. */\nexport function withReaction(counts: ReactionCounts, value: Reaction): ReactionCounts {\n const likes = counts.likes - (counts.mine === 1 ? 1 : 0) + (value === 1 ? 1 : 0);\n const dislikes = counts.dislikes - (counts.mine === -1 ? 1 : 0) + (value === -1 ? 1 : 0);\n return { likes, dislikes, mine: value };\n}\n\nconst commentLists = [\"comments\", \"replies\", \"latest\", \"admin-comments\"];\nconst tokens = new WeakMap<ResourceStore, Map<string, number>>();\n\n/** Sets the caller's reaction to a comment in every list showing it at once; rolls back on failure unless a newer one started. */\nasync function reactToComment(client: ContentKitClient, store: ResourceStore, scope: string, c: Pick<Comment, \"id\" | \"likes\" | \"dislikes\" | \"mine\">, value: Reaction) {\n let t = tokens.get(store);\n if (!t) tokens.set(store, (t = new Map()));\n const token = (t.get(c.id) ?? 0) + 1;\n t.set(c.id, token);\n const reads = new Set<number>();\n const put = (counts: ReactionCounts, rollback = false) =>\n store.patch<Page<Comment>>(\n (tag) => commentLists.includes(tag.type) && tag.readScope === scope,\n (page, readVersion) => {\n if (rollback && !reads.has(readVersion)) return page;\n reads.add(readVersion);\n return { ...page, items: page.items.map((x) => (x.id === c.id ? { ...x, ...counts } : x)) };\n },\n );\n put(withReaction({ likes: c.likes, dislikes: c.dislikes, mine: c.mine }, value));\n try {\n await client.comments.react(c.id, value);\n } catch (e) {\n if (t.get(c.id) === token) put({ likes: c.likes, dislikes: c.dislikes, mine: c.mine }, true);\n throw e;\n }\n}\n\nexport interface UseCommentsOptions {\n sort?: Sort;\n /** One language's thread: the list shows it and new comments join it. */\n language?: string;\n /** Top-level comments per page. Default 20. */\n pageSize?: number;\n client?: ContentKitClient;\n}\n\nexport interface UseComments extends UseList<Comment> {\n /** Comments on the item, or replies to a top-level comment (replyTo); anonymous callers give anonName. */\n post: (body: string, o?: { replyTo?: string; anonName?: string }) => Promise<Comment>;\n edit: (id: string, body: string) => Promise<Comment>;\n /** Leaves a tombstone. */\n remove: (id: string) => Promise<void>;\n /** Sets the caller's reaction at once, rolled back if the server refuses it. */\n react: (comment: Comment, value: Reaction) => Promise<void>;\n /** Restores a deleted comment (moderators). */\n restore: (id: string) => Promise<void>;\n}\n\n/** A target's top-level comments, a page at a time, with every write; new comments and replies land in place. */\nexport function useComments(target: Target, o: UseCommentsOptions = {}): UseComments {\n const { client, store, scope } = useContentScope(o.client);\n const size = o.pageSize ?? 20;\n const sort = o.sort ?? \"newest\";\n const language = o.language || undefined;\n const { type, id, version } = target;\n const key = keyOf(\"comments\", scope, type, id, version, language, sort, size);\n const list = useList(\n store,\n key,\n { type: \"comments\", target: { type, id, version } },\n offsetPages(size, (q, signal) => client.comments.list({ type, id, version }, { ...q, sort, language }, signal)),\n );\n const post = useCallback<UseComments[\"post\"]>(\n (body, p = {}) => client.comments.create({ type, id, version }, { body, reply_to_id: p.replyTo, anon_name: p.anonName, language: p.replyTo ? undefined : language }),\n [client, type, id, version, language],\n );\n const edit = useCallback((cid: string, body: string) => client.comments.edit(cid, body), [client]);\n const remove = useCallback((cid: string) => client.comments.delete(cid), [client]);\n const restore = useCallback((cid: string) => client.comments.restore(cid), [client]);\n const react = useCallback((c: Comment, v: Reaction) => reactToComment(client, store, scope, c, v), [client, store, scope]);\n return { ...list, post, edit, remove, react, restore };\n}\n\n/** A top-level comment's replies, oldest first, a page at a time; enabled false defers the first read. */\nexport function useCommentReplies(id: string, o: { pageSize?: number; enabled?: boolean; client?: ContentKitClient } = {}): UseList<Comment> {\n const { client, store, scope } = useContentScope(o.client);\n const size = o.pageSize ?? 10;\n const key = o.enabled === false ? null : keyOf(\"replies\", scope, id, size);\n return useList(store, key, { type: \"replies\", id }, offsetPages(size, (q, signal) => client.comments.replies(id, q, signal)));\n}\n\n/** Whether the caller may comment on the target, the ban that stops it, and what it may do to others' comments. */\nexport function useCanComment(target: Target, o: { client?: ContentKitClient } = {}) {\n const { client, store, scope } = useContentScope(o.client);\n const { type, id, version } = target;\n const r = useResource<CommentStanding>(store, keyOf(\"standing\", scope, type, id, version), { type: \"standing\", target: { type, id, version } }, (s) =>\n client.comments.standing({ type, id, version }, s),\n );\n return { standing: r.data ?? null, loading: r.loading || !r.loaded, error: r.error, reload: r.reload };\n}\n\n/** The newest published comments across the site, with their items (the host shows titles). */\nexport function useLatestComments(o: { pageSize?: number; client?: ContentKitClient } = {}): UseList<FeedItem> {\n const { client, store, scope } = useContentScope(o.client);\n const size = o.pageSize ?? 20;\n return useList(store, keyOf(\"latest\", scope, size), { type: \"latest\" }, offsetPages(size, (q, signal) => client.comments.latest(q, signal), true));\n}\n\nexport interface UseAdminComments extends UseList<AdminComment> {\n remove: (id: string) => Promise<void>;\n restore: (id: string) => Promise<void>;\n react: (comment: Comment, value: Reaction) => Promise<void>;\n}\n\n/** Staff: every comment, newest first, deleted, held and rejected ones with their bodies (root:comments:moderate); or one content kind's, or one author's. */\nexport function useAdminComments(o: { contentKind?: string; author?: string; pageSize?: number; client?: ContentKitClient } = {}): UseAdminComments {\n const { client, store, scope } = useContentScope(o.client);\n const size = o.pageSize ?? 25;\n const kind = o.contentKind || undefined;\n const author = o.author || undefined;\n const list = useList(\n store,\n keyOf(\"admin-comments\", scope, kind, author, size),\n { type: \"admin-comments\" },\n offsetPages(size, (q, signal) => client.comments.adminList({ ...q, contentKind: kind, author }, signal)),\n );\n const remove = useCallback((id: string) => client.comments.delete(id), [client]);\n const restore = useCallback((id: string) => client.comments.restore(id), [client]);\n const react = useCallback((c: Comment, v: Reaction) => reactToComment(client, store, scope, c, v), [client, store, scope]);\n return { ...list, remove, restore, react };\n}\n\nexport interface UseModerationQueue extends UseList<HeldItem> {\n /** Approves (publishes) or rejects at the revision the queue listed; the item leaves the queue. */\n resolve: (item: HeldItem, decision: Decision, reason?: string) => Promise<void>;\n}\n\n/** Staff: held comments or content awaiting review, oldest first (ModerationReview). */\nexport function useModerationQueue(o: { kind?: HeldKind; pageSize?: number; client?: ContentKitClient } = {}): UseModerationQueue {\n const { client, store, scope } = useContentScope(o.client);\n const kind = o.kind ?? \"comment\";\n const size = o.pageSize ?? 20;\n const list = useList<HeldItem>(store, keyOf(\"held\", scope, kind, size), { type: \"held\" }, async (cursor, signal) => {\n const page = await client.moderation.held({ kind, cursor: typeof cursor === \"string\" ? cursor : undefined, limit: size }, signal);\n return { items: page.items, next: page.next ?? null };\n });\n const resolve = useCallback(async (item: HeldItem, d: Decision, reason?: string) => void (await client.moderation.resolve(item, d, reason)), [client]);\n return { ...list, resolve };\n}\n\nexport interface UseCommentBans extends UseList<CommentBan & { id: string }> {\n ban: (user: string, input?: BanInput) => Promise<CommentBan>;\n lift: (user: string) => Promise<void>;\n}\n\n/** The bans of one scope: the caller's own (\"owner\") or the site's (\"global\", CommentBan), newest first. */\nexport function useCommentBans(o: { scope?: BanScope; pageSize?: number; enabled?: boolean; client?: ContentKitClient } = {}): UseCommentBans {\n const { client, store, scope: readScope } = useContentScope(o.client);\n const scope = o.scope ?? \"global\";\n const size = o.pageSize ?? 25;\n const list = useList(\n store,\n o.enabled === false ? null : keyOf(\"bans\", readScope, scope, size),\n { type: \"bans\", scope },\n offsetPages(size, async (q, signal) => (await client.bans.list(scope, q, signal)).map((b) => ({ ...b, id: b.user_id }))),\n );\n const ban = useCallback((user: string, input?: BanInput) => client.bans.ban(scope, user, input), [client, scope]);\n const lift = useCallback((user: string) => client.bans.lift(scope, user), [client, scope]);\n return { ...list, ban, lift };\n}\n\nexport interface UseGlobalCommentBan {\n /** The user's site-wide ban, expired or not; null when none. */\n ban: CommentBan | null;\n loading: boolean;\n error?: ContentKitError;\n reload: () => void;\n /** Bans them, or replaces their ban. */\n save: (input?: BanInput) => Promise<CommentBan>;\n lift: () => Promise<void>;\n}\n\n/** Staff: one user's site-wide comment ban (root:comments:ban). */\nexport function useGlobalCommentBan(user: string, o: { client?: ContentKitClient } = {}): UseGlobalCommentBan {\n const { client, store, scope } = useContentScope(o.client);\n const r = useResource<CommentBan | null>(store, keyOf(\"ban\", scope, user), { type: \"bans\", scope: \"global\", id: user }, (signal) => client.bans.get(\"global\", user, signal));\n const save = useCallback((input?: BanInput) => client.bans.ban(\"global\", user, input), [client, user]);\n const lift = useCallback(() => client.bans.lift(\"global\", user), [client, user]);\n return { ban: r.data ?? null, loading: r.loading || !r.loaded, error: r.error, reload: r.reload, save, lift };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"comments.js","names":[],"sources":["../../src/react/comments.ts"],"sourcesContent":["import { useCallback } from \"react\";\nimport type { ContentKitClient } from \"../client/client.js\";\nimport type { BanScope, Decision, HeldKind, Reaction, Sort, Target } from \"../client/content/types.js\";\nimport type { ContentKitError } from \"../client/errors.js\";\nimport type { AdminComment, BanInput, BannedWord, BannedWordInput, Comment, CommentBan, CommentStanding, FeedItem, HeldItem, IPBan, IPBanInput, ReactionCounts } from \"../client/generated/wire.js\";\nimport type { Page, ResourceStore } from \"./resources.js\";\nimport { keyOf, offsetPages, useContentScope, useList, useResource, type UseList } from \"./use-resource.js\";\n\n/** Counts after the caller's reaction changes from counts.mine to value. */\nexport function withReaction(counts: ReactionCounts, value: Reaction): ReactionCounts {\n const likes = counts.likes - (counts.mine === 1 ? 1 : 0) + (value === 1 ? 1 : 0);\n const dislikes = counts.dislikes - (counts.mine === -1 ? 1 : 0) + (value === -1 ? 1 : 0);\n return { likes, dislikes, mine: value };\n}\n\nconst commentLists = [\"comments\", \"replies\", \"latest\", \"admin-comments\"];\nconst tokens = new WeakMap<ResourceStore, Map<string, number>>();\n\n/** Sets the caller's reaction to a comment in every list showing it at once; rolls back on failure unless a newer one started. */\nasync function reactToComment(client: ContentKitClient, store: ResourceStore, scope: string, c: Pick<Comment, \"id\" | \"likes\" | \"dislikes\" | \"mine\">, value: Reaction) {\n let t = tokens.get(store);\n if (!t) tokens.set(store, (t = new Map()));\n const token = (t.get(c.id) ?? 0) + 1;\n t.set(c.id, token);\n const reads = new Set<number>();\n const put = (counts: ReactionCounts, rollback = false) =>\n store.patch<Page<Comment>>(\n (tag) => commentLists.includes(tag.type) && tag.readScope === scope,\n (page, readVersion) => {\n if (rollback && !reads.has(readVersion)) return page;\n reads.add(readVersion);\n return { ...page, items: page.items.map((x) => (x.id === c.id ? { ...x, ...counts } : x)) };\n },\n );\n put(withReaction({ likes: c.likes, dislikes: c.dislikes, mine: c.mine }, value));\n try {\n await client.comments.react(c.id, value);\n } catch (e) {\n if (t.get(c.id) === token) put({ likes: c.likes, dislikes: c.dislikes, mine: c.mine }, true);\n throw e;\n }\n}\n\nexport interface UseCommentsOptions {\n sort?: Sort;\n /** One language's thread: the list shows it and new comments join it. */\n language?: string;\n /** Top-level comments per page. Default 20. */\n pageSize?: number;\n client?: ContentKitClient;\n}\n\nexport interface UseComments extends UseList<Comment> {\n /** Comments on the item, or replies to a top-level comment (replyTo); anonymous callers give anonName. */\n post: (body: string, o?: { replyTo?: string; anonName?: string }) => Promise<Comment>;\n edit: (id: string, body: string) => Promise<Comment>;\n /** Leaves a tombstone. */\n remove: (id: string) => Promise<void>;\n /** Sets the caller's reaction at once, rolled back if the server refuses it. */\n react: (comment: Comment, value: Reaction) => Promise<void>;\n /** Restores a deleted comment (moderators). */\n restore: (id: string) => Promise<void>;\n}\n\n/** A target's top-level comments, a page at a time, with every write; new comments and replies land in place. */\nexport function useComments(target: Target, o: UseCommentsOptions = {}): UseComments {\n const { client, store, scope } = useContentScope(o.client);\n const size = o.pageSize ?? 20;\n const sort = o.sort ?? \"newest\";\n const language = o.language || undefined;\n const { type, id, version } = target;\n const key = keyOf(\"comments\", scope, type, id, version, language, sort, size);\n const list = useList(\n store,\n key,\n { type: \"comments\", target: { type, id, version } },\n offsetPages(size, (q, signal) => client.comments.list({ type, id, version }, { ...q, sort, language }, signal)),\n );\n const post = useCallback<UseComments[\"post\"]>(\n (body, p = {}) => client.comments.create({ type, id, version }, { body, reply_to_id: p.replyTo, anon_name: p.anonName, language: p.replyTo ? undefined : language }),\n [client, type, id, version, language],\n );\n const edit = useCallback((cid: string, body: string) => client.comments.edit(cid, body), [client]);\n const remove = useCallback((cid: string) => client.comments.delete(cid), [client]);\n const restore = useCallback((cid: string) => client.comments.restore(cid), [client]);\n const react = useCallback((c: Comment, v: Reaction) => reactToComment(client, store, scope, c, v), [client, store, scope]);\n return { ...list, post, edit, remove, react, restore };\n}\n\n/** A top-level comment's replies, oldest first, a page at a time; enabled false defers the first read. */\nexport function useCommentReplies(id: string, o: { pageSize?: number; enabled?: boolean; client?: ContentKitClient } = {}): UseList<Comment> {\n const { client, store, scope } = useContentScope(o.client);\n const size = o.pageSize ?? 10;\n const key = o.enabled === false ? null : keyOf(\"replies\", scope, id, size);\n return useList(store, key, { type: \"replies\", id }, offsetPages(size, (q, signal) => client.comments.replies(id, q, signal)));\n}\n\n/** Whether the caller may comment on the target, the ban that stops it, and what it may do to others' comments. */\nexport function useCanComment(target: Target, o: { client?: ContentKitClient } = {}) {\n const { client, store, scope } = useContentScope(o.client);\n const { type, id, version } = target;\n const r = useResource<CommentStanding>(store, keyOf(\"standing\", scope, type, id, version), { type: \"standing\", target: { type, id, version } }, (s) =>\n client.comments.standing({ type, id, version }, s),\n );\n return { standing: r.data ?? null, loading: r.loading || !r.loaded, error: r.error, reload: r.reload };\n}\n\n/** The newest published comments across the site, with their items (the host shows titles). */\nexport function useLatestComments(o: { pageSize?: number; client?: ContentKitClient } = {}): UseList<FeedItem> {\n const { client, store, scope } = useContentScope(o.client);\n const size = o.pageSize ?? 20;\n return useList(store, keyOf(\"latest\", scope, size), { type: \"latest\" }, offsetPages(size, (q, signal) => client.comments.latest(q, signal), true));\n}\n\nexport interface UseAdminComments extends UseList<AdminComment> {\n remove: (id: string) => Promise<void>;\n restore: (id: string) => Promise<void>;\n react: (comment: Comment, value: Reaction) => Promise<void>;\n}\n\n/** Staff: every comment, newest first, deleted, held and rejected ones with their bodies (root:comments:moderate); or one content kind's, or one author's. */\nexport function useAdminComments(o: { contentKind?: string; author?: string; pageSize?: number; client?: ContentKitClient } = {}): UseAdminComments {\n const { client, store, scope } = useContentScope(o.client);\n const size = o.pageSize ?? 25;\n const kind = o.contentKind || undefined;\n const author = o.author || undefined;\n const list = useList(\n store,\n keyOf(\"admin-comments\", scope, kind, author, size),\n { type: \"admin-comments\" },\n offsetPages(size, (q, signal) => client.comments.adminList({ ...q, contentKind: kind, author }, signal)),\n );\n const remove = useCallback((id: string) => client.comments.delete(id), [client]);\n const restore = useCallback((id: string) => client.comments.restore(id), [client]);\n const react = useCallback((c: Comment, v: Reaction) => reactToComment(client, store, scope, c, v), [client, store, scope]);\n return { ...list, remove, restore, react };\n}\n\nexport interface UseModerationQueue extends UseList<HeldItem> {\n /** Approves (publishes) or rejects at the revision the queue listed; the item leaves the queue. */\n resolve: (item: HeldItem, decision: Decision, reason?: string) => Promise<void>;\n}\n\n/** Staff: held comments or content awaiting review, oldest first (ModerationReview). */\nexport function useModerationQueue(o: { kind?: HeldKind; pageSize?: number; client?: ContentKitClient } = {}): UseModerationQueue {\n const { client, store, scope } = useContentScope(o.client);\n const kind = o.kind ?? \"comment\";\n const size = o.pageSize ?? 20;\n const list = useList<HeldItem>(store, keyOf(\"held\", scope, kind, size), { type: \"held\" }, async (cursor, signal) => {\n const page = await client.moderation.held({ kind, cursor: typeof cursor === \"string\" ? cursor : undefined, limit: size }, signal);\n return { items: page.items, next: page.next ?? null };\n });\n const resolve = useCallback(async (item: HeldItem, d: Decision, reason?: string) => void (await client.moderation.resolve(item, d, reason)), [client]);\n return { ...list, resolve };\n}\n\nexport interface UseCommentBans extends UseList<CommentBan & { id: string }> {\n ban: (user: string, input?: BanInput) => Promise<CommentBan>;\n lift: (user: string) => Promise<void>;\n}\n\n/** The bans of one scope: the caller's own (\"owner\") or the site's (\"global\", CommentBan), newest first. */\nexport function useCommentBans(o: { scope?: BanScope; pageSize?: number; enabled?: boolean; client?: ContentKitClient } = {}): UseCommentBans {\n const { client, store, scope: readScope } = useContentScope(o.client);\n const scope = o.scope ?? \"global\";\n const size = o.pageSize ?? 25;\n const list = useList(\n store,\n o.enabled === false ? null : keyOf(\"bans\", readScope, scope, size),\n { type: \"bans\", scope },\n offsetPages(size, async (q, signal) => (await client.bans.list(scope, q, signal)).map((b) => ({ ...b, id: b.user_id }))),\n );\n const ban = useCallback((user: string, input?: BanInput) => client.bans.ban(scope, user, input), [client, scope]);\n const lift = useCallback((user: string) => client.bans.lift(scope, user), [client, scope]);\n return { ...list, ban, lift };\n}\n\nexport interface UseBannedWords {\n words: BannedWord[];\n loading: boolean;\n error?: ContentKitError;\n reload: () => void;\n /** Bans words, or changes what banned ones do. */\n ban: (words: BannedWordInput[]) => Promise<BannedWord[]>;\n unban: (words: string[]) => Promise<void>;\n}\n\n/** Staff: the banned words every comment and answer meets (root:comments:moderate). */\nexport function useBannedWords(o: { enabled?: boolean; client?: ContentKitClient } = {}): UseBannedWords {\n const { client, store, scope } = useContentScope(o.client);\n const r = useResource<BannedWord[]>(store, o.enabled === false ? null : keyOf(\"moderation-rules\", scope, \"words\"), { type: \"moderation-rules\", scope: \"words\" }, (signal) =>\n client.moderation.bannedWords(signal),\n );\n const ban = useCallback((words: BannedWordInput[]) => client.moderation.banWords(words), [client]);\n const unban = useCallback((words: string[]) => client.moderation.unbanWords(words), [client]);\n return { words: r.data ?? [], loading: o.enabled !== false && (r.loading || !r.loaded), error: r.error, reload: r.reload, ban, unban };\n}\n\nexport interface UseIPBans {\n bans: IPBan[];\n loading: boolean;\n error?: ContentKitError;\n reload: () => void;\n /** Bans addresses or networks, or replaces their bans. */\n ban: (bans: IPBanInput[]) => Promise<IPBan[]>;\n lift: (networks: string[]) => Promise<void>;\n}\n\n/** Staff: the IP bans on commenting and answering, newest first (root:comments:moderate). */\nexport function useIPBans(o: { enabled?: boolean; client?: ContentKitClient } = {}): UseIPBans {\n const { client, store, scope } = useContentScope(o.client);\n const r = useResource<IPBan[]>(store, o.enabled === false ? null : keyOf(\"moderation-rules\", scope, \"ips\"), { type: \"moderation-rules\", scope: \"ips\" }, (signal) =>\n client.moderation.ipBans(signal),\n );\n const ban = useCallback((bans: IPBanInput[]) => client.moderation.banIPs(bans), [client]);\n const lift = useCallback((networks: string[]) => client.moderation.liftIPBans(networks), [client]);\n return { bans: r.data ?? [], loading: o.enabled !== false && (r.loading || !r.loaded), error: r.error, reload: r.reload, ban, lift };\n}\n\nexport interface UseGlobalCommentBan {\n /** The user's site-wide ban, expired or not; null when none. */\n ban: CommentBan | null;\n loading: boolean;\n error?: ContentKitError;\n reload: () => void;\n /** Bans them, or replaces their ban. */\n save: (input?: BanInput) => Promise<CommentBan>;\n lift: () => Promise<void>;\n}\n\n/** Staff: one user's site-wide comment ban (root:comments:ban). */\nexport function useGlobalCommentBan(user: string, o: { client?: ContentKitClient } = {}): UseGlobalCommentBan {\n const { client, store, scope } = useContentScope(o.client);\n const r = useResource<CommentBan | null>(store, keyOf(\"ban\", scope, user), { type: \"bans\", scope: \"global\", id: user }, (signal) => client.bans.get(\"global\", user, signal));\n const save = useCallback((input?: BanInput) => client.bans.ban(\"global\", user, input), [client, user]);\n const lift = useCallback(() => client.bans.lift(\"global\", user), [client, user]);\n return { ban: r.data ?? null, loading: r.loading || !r.loaded, error: r.error, reload: r.reload, save, lift };\n}\n"],"mappings":";;;AASA,SAAgB,EAAa,GAAwB,GAAiC;CAGpF,OAAO;EAAE,OAFK,EAAO,QAAS,IAAO,SAAS,KAAc,QAAU;EAEtD,UADC,EAAO,WAAY,IAAO,SAAS,MAAe,QAAU;EACnD,MAAM;CAAM;AACxC;AAEA,IAAM,IAAe;CAAC;CAAY;CAAW;CAAU;AAAgB,GACjE,oBAAS,IAAI,QAA4C;AAG/D,eAAe,EAAe,GAA0B,GAAsB,GAAe,GAAwD,GAAiB;CACpK,IAAI,IAAI,EAAO,IAAI,CAAK;CACxB,AAAK,KAAG,EAAO,IAAI,GAAQ,oBAAI,IAAI,IAAI,CAAE;CACzC,IAAM,KAAS,EAAE,IAAI,EAAE,EAAE,KAAK,KAAK;CACnC,EAAE,IAAI,EAAE,IAAI,CAAK;CACjB,IAAM,oBAAQ,IAAI,IAAY,GACxB,KAAO,GAAwB,IAAW,OAC9C,EAAM,OACH,MAAQ,EAAa,SAAS,EAAI,IAAI,KAAK,EAAI,cAAc,IAC7D,GAAM,MACD,KAAY,CAAC,EAAM,IAAI,CAAW,IAAU,KAChD,EAAM,IAAI,CAAW,GACd;EAAE,GAAG;EAAM,OAAO,EAAK,MAAM,KAAK,MAAO,EAAE,OAAO,EAAE,KAAK;GAAE,GAAG;GAAG,GAAG;EAAO,IAAI,CAAE;CAAE,EAE9F;CACF,EAAI,EAAa;EAAE,OAAO,EAAE;EAAO,UAAU,EAAE;EAAU,MAAM,EAAE;CAAK,GAAG,CAAK,CAAC;CAC/E,IAAI;EACF,MAAM,EAAO,SAAS,MAAM,EAAE,IAAI,CAAK;CACzC,SAAS,GAAG;EAEV,MADI,EAAE,IAAI,EAAE,EAAE,MAAM,KAAO,EAAI;GAAE,OAAO,EAAE;GAAO,UAAU,EAAE;GAAU,MAAM,EAAE;EAAK,GAAG,EAAI,GACrF;CACR;AACF;AAwBA,SAAgB,EAAY,GAAgB,IAAwB,CAAC,GAAgB;CACnF,IAAM,EAAE,WAAQ,UAAO,aAAU,EAAgB,EAAE,MAAM,GACnD,IAAO,EAAE,YAAY,IACrB,IAAO,EAAE,QAAQ,UACjB,IAAW,EAAE,YAAY,KAAA,GACzB,EAAE,SAAM,OAAI,eAAY,GACxB,IAAM,EAAM,YAAY,GAAO,GAAM,GAAI,GAAS,GAAU,GAAM,CAAI,GACtE,IAAO,EACX,GACA,GACA;EAAE,MAAM;EAAY,QAAQ;GAAE;GAAM;GAAI;EAAQ;CAAE,GAClD,EAAY,IAAO,GAAG,MAAW,EAAO,SAAS,KAAK;EAAE;EAAM;EAAI;CAAQ,GAAG;EAAE,GAAG;EAAG;EAAM;CAAS,GAAG,CAAM,CAAC,CAChH,GACM,IAAO,GACV,GAAM,IAAI,CAAC,MAAM,EAAO,SAAS,OAAO;EAAE;EAAM;EAAI;CAAQ,GAAG;EAAE;EAAM,aAAa,EAAE;EAAS,WAAW,EAAE;EAAU,UAAU,EAAE,UAAU,KAAA,IAAY;CAAS,CAAC,GACnK;EAAC;EAAQ;EAAM;EAAI;EAAS;CAAQ,CACtC,GACM,IAAO,GAAa,GAAa,MAAiB,EAAO,SAAS,KAAK,GAAK,CAAI,GAAG,CAAC,CAAM,CAAC,GAC3F,IAAS,GAAa,MAAgB,EAAO,SAAS,OAAO,CAAG,GAAG,CAAC,CAAM,CAAC,GAC3E,IAAU,GAAa,MAAgB,EAAO,SAAS,QAAQ,CAAG,GAAG,CAAC,CAAM,CAAC,GAC7E,IAAQ,GAAa,GAAY,MAAgB,EAAe,GAAQ,GAAO,GAAO,GAAG,CAAC,GAAG;EAAC;EAAQ;EAAO;CAAK,CAAC;CACzH,OAAO;EAAE,GAAG;EAAM;EAAM;EAAM;EAAQ;EAAO;CAAQ;AACvD;AAGA,SAAgB,EAAkB,GAAY,IAAyE,CAAC,GAAqB;CAC3I,IAAM,EAAE,WAAQ,UAAO,aAAU,EAAgB,EAAE,MAAM,GACnD,IAAO,EAAE,YAAY,IACrB,IAAM,EAAE,YAAY,KAAQ,OAAO,EAAM,WAAW,GAAO,GAAI,CAAI;CACzE,OAAO,EAAQ,GAAO,GAAK;EAAE,MAAM;EAAW;CAAG,GAAG,EAAY,IAAO,GAAG,MAAW,EAAO,SAAS,QAAQ,GAAI,GAAG,CAAM,CAAC,CAAC;AAC9H;AAGA,SAAgB,EAAc,GAAgB,IAAmC,CAAC,GAAG;CACnF,IAAM,EAAE,WAAQ,UAAO,aAAU,EAAgB,EAAE,MAAM,GACnD,EAAE,SAAM,OAAI,eAAY,GACxB,IAAI,EAA6B,GAAO,EAAM,YAAY,GAAO,GAAM,GAAI,CAAO,GAAG;EAAE,MAAM;EAAY,QAAQ;GAAE;GAAM;GAAI;EAAQ;CAAE,IAAI,MAC/I,EAAO,SAAS,SAAS;EAAE;EAAM;EAAI;CAAQ,GAAG,CAAC,CACnD;CACA,OAAO;EAAE,UAAU,EAAE,QAAQ;EAAM,SAAS,EAAE,WAAW,CAAC,EAAE;EAAQ,OAAO,EAAE;EAAO,QAAQ,EAAE;CAAO;AACvG;AAGA,SAAgB,EAAkB,IAAsD,CAAC,GAAsB;CAC7G,IAAM,EAAE,WAAQ,UAAO,aAAU,EAAgB,EAAE,MAAM,GACnD,IAAO,EAAE,YAAY;CAC3B,OAAO,EAAQ,GAAO,EAAM,UAAU,GAAO,CAAI,GAAG,EAAE,MAAM,SAAS,GAAG,EAAY,IAAO,GAAG,MAAW,EAAO,SAAS,OAAO,GAAG,CAAM,GAAG,EAAI,CAAC;AACnJ;AASA,SAAgB,EAAiB,IAA6F,CAAC,GAAqB;CAClJ,IAAM,EAAE,WAAQ,UAAO,aAAU,EAAgB,EAAE,MAAM,GACnD,IAAO,EAAE,YAAY,IACrB,IAAO,EAAE,eAAe,KAAA,GACxB,IAAS,EAAE,UAAU,KAAA,GACrB,IAAO,EACX,GACA,EAAM,kBAAkB,GAAO,GAAM,GAAQ,CAAI,GACjD,EAAE,MAAM,iBAAiB,GACzB,EAAY,IAAO,GAAG,MAAW,EAAO,SAAS,UAAU;EAAE,GAAG;EAAG,aAAa;EAAM;CAAO,GAAG,CAAM,CAAC,CACzG,GACM,IAAS,GAAa,MAAe,EAAO,SAAS,OAAO,CAAE,GAAG,CAAC,CAAM,CAAC,GACzE,IAAU,GAAa,MAAe,EAAO,SAAS,QAAQ,CAAE,GAAG,CAAC,CAAM,CAAC,GAC3E,IAAQ,GAAa,GAAY,MAAgB,EAAe,GAAQ,GAAO,GAAO,GAAG,CAAC,GAAG;EAAC;EAAQ;EAAO;CAAK,CAAC;CACzH,OAAO;EAAE,GAAG;EAAM;EAAQ;EAAS;CAAM;AAC3C;AAQA,SAAgB,EAAmB,IAAuE,CAAC,GAAuB;CAChI,IAAM,EAAE,WAAQ,UAAO,aAAU,EAAgB,EAAE,MAAM,GACnD,IAAO,EAAE,QAAQ,WACjB,IAAO,EAAE,YAAY,IACrB,IAAO,EAAkB,GAAO,EAAM,QAAQ,GAAO,GAAM,CAAI,GAAG,EAAE,MAAM,OAAO,GAAG,OAAO,GAAQ,MAAW;EAClH,IAAM,IAAO,MAAM,EAAO,WAAW,KAAK;GAAE;GAAM,QAAQ,OAAO,KAAW,WAAW,IAAS,KAAA;GAAW,OAAO;EAAK,GAAG,CAAM;EAChI,OAAO;GAAE,OAAO,EAAK;GAAO,MAAM,EAAK,QAAQ;EAAK;CACtD,CAAC,GACK,IAAU,EAAY,OAAO,GAAgB,GAAa,MAAoB,KAAM,MAAM,EAAO,WAAW,QAAQ,GAAM,GAAG,CAAM,GAAI,CAAC,CAAM,CAAC;CACrJ,OAAO;EAAE,GAAG;EAAM;CAAQ;AAC5B;AAQA,SAAgB,EAAe,IAA2F,CAAC,GAAmB;CAC5I,IAAM,EAAE,WAAQ,UAAO,OAAO,MAAc,EAAgB,EAAE,MAAM,GAC9D,IAAQ,EAAE,SAAS,UACnB,IAAO,EAAE,YAAY,IACrB,IAAO,EACX,GACA,EAAE,YAAY,KAAQ,OAAO,EAAM,QAAQ,GAAW,GAAO,CAAI,GACjE;EAAE,MAAM;EAAQ;CAAM,GACtB,EAAY,GAAM,OAAO,GAAG,OAAY,MAAM,EAAO,KAAK,KAAK,GAAO,GAAG,CAAM,EAAA,CAAG,KAAK,OAAO;EAAE,GAAG;EAAG,IAAI,EAAE;CAAQ,EAAE,CAAC,CACzH,GACM,IAAM,GAAa,GAAc,MAAqB,EAAO,KAAK,IAAI,GAAO,GAAM,CAAK,GAAG,CAAC,GAAQ,CAAK,CAAC,GAC1G,IAAO,GAAa,MAAiB,EAAO,KAAK,KAAK,GAAO,CAAI,GAAG,CAAC,GAAQ,CAAK,CAAC;CACzF,OAAO;EAAE,GAAG;EAAM;EAAK;CAAK;AAC9B;AAaA,SAAgB,EAAe,IAAsD,CAAC,GAAmB;CACvG,IAAM,EAAE,WAAQ,UAAO,aAAU,EAAgB,EAAE,MAAM,GACnD,IAAI,EAA0B,GAAO,EAAE,YAAY,KAAQ,OAAO,EAAM,oBAAoB,GAAO,OAAO,GAAG;EAAE,MAAM;EAAoB,OAAO;CAAQ,IAAI,MAChK,EAAO,WAAW,YAAY,CAAM,CACtC,GACM,IAAM,GAAa,MAA6B,EAAO,WAAW,SAAS,CAAK,GAAG,CAAC,CAAM,CAAC,GAC3F,IAAQ,GAAa,MAAoB,EAAO,WAAW,WAAW,CAAK,GAAG,CAAC,CAAM,CAAC;CAC5F,OAAO;EAAE,OAAO,EAAE,QAAQ,CAAC;EAAG,SAAS,EAAE,YAAY,OAAU,EAAE,WAAW,CAAC,EAAE;EAAS,OAAO,EAAE;EAAO,QAAQ,EAAE;EAAQ;EAAK;CAAM;AACvI;AAaA,SAAgB,EAAU,IAAsD,CAAC,GAAc;CAC7F,IAAM,EAAE,WAAQ,UAAO,aAAU,EAAgB,EAAE,MAAM,GACnD,IAAI,EAAqB,GAAO,EAAE,YAAY,KAAQ,OAAO,EAAM,oBAAoB,GAAO,KAAK,GAAG;EAAE,MAAM;EAAoB,OAAO;CAAM,IAAI,MACvJ,EAAO,WAAW,OAAO,CAAM,CACjC,GACM,IAAM,GAAa,MAAuB,EAAO,WAAW,OAAO,CAAI,GAAG,CAAC,CAAM,CAAC,GAClF,IAAO,GAAa,MAAuB,EAAO,WAAW,WAAW,CAAQ,GAAG,CAAC,CAAM,CAAC;CACjG,OAAO;EAAE,MAAM,EAAE,QAAQ,CAAC;EAAG,SAAS,EAAE,YAAY,OAAU,EAAE,WAAW,CAAC,EAAE;EAAS,OAAO,EAAE;EAAO,QAAQ,EAAE;EAAQ;EAAK;CAAK;AACrI;AAcA,SAAgB,EAAoB,GAAc,IAAmC,CAAC,GAAwB;CAC5G,IAAM,EAAE,WAAQ,UAAO,aAAU,EAAgB,EAAE,MAAM,GACnD,IAAI,EAA+B,GAAO,EAAM,OAAO,GAAO,CAAI,GAAG;EAAE,MAAM;EAAQ,OAAO;EAAU,IAAI;CAAK,IAAI,MAAW,EAAO,KAAK,IAAI,UAAU,GAAM,CAAM,CAAC,GACrK,IAAO,GAAa,MAAqB,EAAO,KAAK,IAAI,UAAU,GAAM,CAAK,GAAG,CAAC,GAAQ,CAAI,CAAC,GAC/F,IAAO,QAAkB,EAAO,KAAK,KAAK,UAAU,CAAI,GAAG,CAAC,GAAQ,CAAI,CAAC;CAC/E,OAAO;EAAE,KAAK,EAAE,QAAQ;EAAM,SAAS,EAAE,WAAW,CAAC,EAAE;EAAQ,OAAO,EAAE;EAAO,QAAQ,EAAE;EAAQ;EAAM;CAAK;AAC9G"}