@overcrow/mcp 0.0.0-stage → 0.1.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 (461) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +90 -2
  3. package/dist/audit/index.js +103 -0
  4. package/dist/audit/lexer.js +417 -0
  5. package/dist/audit/project.js +200 -0
  6. package/dist/audit/rules.js +661 -0
  7. package/dist/bootstrap/download.js +229 -0
  8. package/dist/bootstrap/install.js +280 -0
  9. package/dist/bootstrap/pin.js +85 -0
  10. package/dist/bootstrap/pin.json +119 -0
  11. package/dist/bootstrap/zip.js +258 -0
  12. package/dist/cli.js +200 -0
  13. package/dist/confine.js +244 -0
  14. package/dist/content/docs/en/cli.md +353 -0
  15. package/dist/content/docs/en/dev-channel.md +130 -0
  16. package/dist/content/docs/en/elements.md +469 -0
  17. package/dist/content/docs/en/forms.md +328 -0
  18. package/dist/content/docs/en/guide.md +253 -0
  19. package/dist/content/docs/en/index.md +85 -0
  20. package/dist/content/docs/en/limits.md +241 -0
  21. package/dist/content/docs/en/logic.md +446 -0
  22. package/dist/content/docs/en/manifest.md +381 -0
  23. package/dist/content/docs/en/mcp.md +165 -0
  24. package/dist/content/docs/en/package.md +131 -0
  25. package/dist/content/docs/en/publishing.md +162 -0
  26. package/dist/content/docs/en/reference.md +55 -0
  27. package/dist/content/docs/en/result-shapes.md +332 -0
  28. package/dist/content/docs/en/sdk.md +295 -0
  29. package/dist/content/docs/en/security.md +115 -0
  30. package/dist/content/docs/en/service-reference.md +344 -0
  31. package/dist/content/docs/en/services.md +517 -0
  32. package/dist/content/docs/en/style-properties.md +270 -0
  33. package/dist/content/docs/en/style.md +266 -0
  34. package/dist/content/docs/en/testing.md +400 -0
  35. package/dist/content/docs/en/view.md +338 -0
  36. package/dist/content/docs/en/widgets.md +76 -0
  37. package/dist/content/docs/fr/cli.md +367 -0
  38. package/dist/content/docs/fr/dev-channel.md +140 -0
  39. package/dist/content/docs/fr/elements.md +475 -0
  40. package/dist/content/docs/fr/forms.md +341 -0
  41. package/dist/content/docs/fr/guide.md +264 -0
  42. package/dist/content/docs/fr/index.md +90 -0
  43. package/dist/content/docs/fr/limits.md +244 -0
  44. package/dist/content/docs/fr/logic.md +461 -0
  45. package/dist/content/docs/fr/manifest.md +392 -0
  46. package/dist/content/docs/fr/mcp.md +173 -0
  47. package/dist/content/docs/fr/package.md +139 -0
  48. package/dist/content/docs/fr/publishing.md +172 -0
  49. package/dist/content/docs/fr/reference.md +59 -0
  50. package/dist/content/docs/fr/result-shapes.md +333 -0
  51. package/dist/content/docs/fr/sdk.md +301 -0
  52. package/dist/content/docs/fr/security.md +129 -0
  53. package/dist/content/docs/fr/service-reference.md +346 -0
  54. package/dist/content/docs/fr/services.md +540 -0
  55. package/dist/content/docs/fr/style-properties.md +272 -0
  56. package/dist/content/docs/fr/style.md +281 -0
  57. package/dist/content/docs/fr/testing.md +417 -0
  58. package/dist/content/docs/fr/view.md +352 -0
  59. package/dist/content/docs/fr/widgets.md +81 -0
  60. package/dist/content/examples/clock/LICENSE +21 -0
  61. package/dist/content/examples/clock/README.md +75 -0
  62. package/dist/content/examples/clock/listing.json +19 -0
  63. package/dist/content/examples/clock/locales/en.json +4 -0
  64. package/dist/content/examples/clock/locales/fr.json +4 -0
  65. package/dist/content/examples/clock/logic.ts +85 -0
  66. package/dist/content/examples/clock/manifest.json +44 -0
  67. package/dist/content/examples/clock/package.json +16 -0
  68. package/dist/content/examples/clock/style.ocss +26 -0
  69. package/dist/content/examples/clock/tests/hidden.scenario.json +68 -0
  70. package/dist/content/examples/clock/tests/load-logic.mjs +22 -0
  71. package/dist/content/examples/clock/tests/logic.test.mjs +89 -0
  72. package/dist/content/examples/clock/tests/options.scenario.json +108 -0
  73. package/dist/content/examples/clock/tests/preview.scenario.json +25 -0
  74. package/dist/content/examples/clock/tests/render.scenario.json +94 -0
  75. package/dist/content/examples/clock/tests/summer-time.scenario.json +74 -0
  76. package/dist/content/examples/clock/tests/ticks.scenario.json +120 -0
  77. package/dist/content/examples/clock/tsconfig.json +16 -0
  78. package/dist/content/examples/clock/view.ocml +8 -0
  79. package/dist/content/examples/countdown/LICENSE +20 -0
  80. package/dist/content/examples/countdown/listing.json +10 -0
  81. package/dist/content/examples/countdown/locales/en.json +15 -0
  82. package/dist/content/examples/countdown/locales/fr.json +15 -0
  83. package/dist/content/examples/countdown/logic.ts +179 -0
  84. package/dist/content/examples/countdown/manifest.json +34 -0
  85. package/dist/content/examples/countdown/package.json +15 -0
  86. package/dist/content/examples/countdown/style.ocss +94 -0
  87. package/dist/content/examples/countdown/tsconfig.json +16 -0
  88. package/dist/content/examples/countdown/view.ocml +42 -0
  89. package/dist/content/examples/fps/LICENSE +21 -0
  90. package/dist/content/examples/fps/README.md +64 -0
  91. package/dist/content/examples/fps/listing.json +19 -0
  92. package/dist/content/examples/fps/locales/en.json +11 -0
  93. package/dist/content/examples/fps/locales/fr.json +11 -0
  94. package/dist/content/examples/fps/logic.ts +64 -0
  95. package/dist/content/examples/fps/manifest.json +30 -0
  96. package/dist/content/examples/fps/package.json +16 -0
  97. package/dist/content/examples/fps/style.ocss +31 -0
  98. package/dist/content/examples/fps/tests/label.scenario.json +63 -0
  99. package/dist/content/examples/fps/tests/load-logic.mjs +22 -0
  100. package/dist/content/examples/fps/tests/logic.test.mjs +60 -0
  101. package/dist/content/examples/fps/tests/preview.scenario.json +29 -0
  102. package/dist/content/examples/fps/tests/refused.scenario.json +20 -0
  103. package/dist/content/examples/fps/tests/render.scenario.json +96 -0
  104. package/dist/content/examples/fps/tests/stale.scenario.json +91 -0
  105. package/dist/content/examples/fps/tests/unavailable.scenario.json +111 -0
  106. package/dist/content/examples/fps/tsconfig.json +16 -0
  107. package/dist/content/examples/fps/view.ocml +11 -0
  108. package/dist/content/examples/media/LICENSE +21 -0
  109. package/dist/content/examples/media/README.md +147 -0
  110. package/dist/content/examples/media/listing.json +19 -0
  111. package/dist/content/examples/media/locales/en.json +11 -0
  112. package/dist/content/examples/media/locales/fr.json +11 -0
  113. package/dist/content/examples/media/logic.ts +191 -0
  114. package/dist/content/examples/media/manifest.json +35 -0
  115. package/dist/content/examples/media/package.json +16 -0
  116. package/dist/content/examples/media/style.ocss +148 -0
  117. package/dist/content/examples/media/tests/covers.scenario.json +144 -0
  118. package/dist/content/examples/media/tests/gesture.scenario.json +173 -0
  119. package/dist/content/examples/media/tests/load-logic.mjs +22 -0
  120. package/dist/content/examples/media/tests/logic.test.mjs +176 -0
  121. package/dist/content/examples/media/tests/marquee.scenario.json +127 -0
  122. package/dist/content/examples/media/tests/preview.scenario.json +52 -0
  123. package/dist/content/examples/media/tests/read-only.scenario.json +53 -0
  124. package/dist/content/examples/media/tests/refused.scenario.json +22 -0
  125. package/dist/content/examples/media/tests/refused.test.mjs +31 -0
  126. package/dist/content/examples/media/tests/render.scenario.json +136 -0
  127. package/dist/content/examples/media/tests/states.scenario.json +257 -0
  128. package/dist/content/examples/media/tests/unavailable.scenario.json +120 -0
  129. package/dist/content/examples/media/tsconfig.json +16 -0
  130. package/dist/content/examples/media/view.ocml +41 -0
  131. package/dist/content/examples/notes/LICENSE +21 -0
  132. package/dist/content/examples/notes/README.md +219 -0
  133. package/dist/content/examples/notes/listing.json +19 -0
  134. package/dist/content/examples/notes/locales/en.json +25 -0
  135. package/dist/content/examples/notes/locales/fr.json +25 -0
  136. package/dist/content/examples/notes/logic.ts +799 -0
  137. package/dist/content/examples/notes/manifest.json +40 -0
  138. package/dist/content/examples/notes/package.json +16 -0
  139. package/dist/content/examples/notes/style.ocss +469 -0
  140. package/dist/content/examples/notes/tests/checks.scenario.json +237 -0
  141. package/dist/content/examples/notes/tests/create.scenario.json +260 -0
  142. package/dist/content/examples/notes/tests/delete.scenario.json +342 -0
  143. package/dist/content/examples/notes/tests/drafts.scenario.json +714 -0
  144. package/dist/content/examples/notes/tests/editor.scenario.json +232 -0
  145. package/dist/content/examples/notes/tests/frame.scenario.json +93 -0
  146. package/dist/content/examples/notes/tests/interactive.scenario.json +344 -0
  147. package/dist/content/examples/notes/tests/limits.scenario.json +1351 -0
  148. package/dist/content/examples/notes/tests/load-logic.mjs +22 -0
  149. package/dist/content/examples/notes/tests/logic.test.mjs +514 -0
  150. package/dist/content/examples/notes/tests/preview.scenario.json +99 -0
  151. package/dist/content/examples/notes/tests/read-only.scenario.json +105 -0
  152. package/dist/content/examples/notes/tests/refused.scenario.json +33 -0
  153. package/dist/content/examples/notes/tests/save.scenario.json +378 -0
  154. package/dist/content/examples/notes/tests/states.scenario.json +640 -0
  155. package/dist/content/examples/notes/tests/typing.scenario.json +562 -0
  156. package/dist/content/examples/notes/tests/unavailable.scenario.json +246 -0
  157. package/dist/content/examples/notes/tsconfig.json +16 -0
  158. package/dist/content/examples/notes/view.ocml +154 -0
  159. package/dist/content/examples/performance/LICENSE +21 -0
  160. package/dist/content/examples/performance/README.md +134 -0
  161. package/dist/content/examples/performance/listing.json +19 -0
  162. package/dist/content/examples/performance/locales/en.json +20 -0
  163. package/dist/content/examples/performance/locales/fr.json +20 -0
  164. package/dist/content/examples/performance/logic.ts +210 -0
  165. package/dist/content/examples/performance/manifest.json +83 -0
  166. package/dist/content/examples/performance/package.json +16 -0
  167. package/dist/content/examples/performance/style.ocss +75 -0
  168. package/dist/content/examples/performance/tests/fahrenheit.scenario.json +110 -0
  169. package/dist/content/examples/performance/tests/horizontal.scenario.json +96 -0
  170. package/dist/content/examples/performance/tests/load-logic.mjs +22 -0
  171. package/dist/content/examples/performance/tests/logic.test.mjs +226 -0
  172. package/dist/content/examples/performance/tests/menu.scenario.json +170 -0
  173. package/dist/content/examples/performance/tests/no-fps-source.scenario.json +47 -0
  174. package/dist/content/examples/performance/tests/preview.scenario.json +45 -0
  175. package/dist/content/examples/performance/tests/refused.scenario.json +20 -0
  176. package/dist/content/examples/performance/tests/refused.test.mjs +19 -0
  177. package/dist/content/examples/performance/tests/region.scenario.json +89 -0
  178. package/dist/content/examples/performance/tests/render.scenario.json +112 -0
  179. package/dist/content/examples/performance/tests/sources.scenario.json +151 -0
  180. package/dist/content/examples/performance/tests/stale.scenario.json +111 -0
  181. package/dist/content/examples/performance/tests/telemetry-only.scenario.json +44 -0
  182. package/dist/content/examples/performance/tests/thresholds.scenario.json +147 -0
  183. package/dist/content/examples/performance/tests/waiting.scenario.json +105 -0
  184. package/dist/content/examples/performance/tsconfig.json +16 -0
  185. package/dist/content/examples/performance/view.ocml +18 -0
  186. package/dist/content/examples/playervox-journal/LICENSE +21 -0
  187. package/dist/content/examples/playervox-journal/README.md +150 -0
  188. package/dist/content/examples/playervox-journal/listing.json +19 -0
  189. package/dist/content/examples/playervox-journal/locales/en.json +25 -0
  190. package/dist/content/examples/playervox-journal/locales/fr.json +25 -0
  191. package/dist/content/examples/playervox-journal/logic.ts +556 -0
  192. package/dist/content/examples/playervox-journal/manifest.json +19 -0
  193. package/dist/content/examples/playervox-journal/package.json +16 -0
  194. package/dist/content/examples/playervox-journal/style.ocss +142 -0
  195. package/dist/content/examples/playervox-journal/tests/changes.scenario.json +482 -0
  196. package/dist/content/examples/playervox-journal/tests/delete.scenario.json +432 -0
  197. package/dist/content/examples/playervox-journal/tests/frame.scenario.json +169 -0
  198. package/dist/content/examples/playervox-journal/tests/load-logic.mjs +22 -0
  199. package/dist/content/examples/playervox-journal/tests/logic.test.mjs +349 -0
  200. package/dist/content/examples/playervox-journal/tests/paging.scenario.json +428 -0
  201. package/dist/content/examples/playervox-journal/tests/preview.scenario.json +184 -0
  202. package/dist/content/examples/playervox-journal/tests/read-only.scenario.json +157 -0
  203. package/dist/content/examples/playervox-journal/tests/refused.scenario.json +39 -0
  204. package/dist/content/examples/playervox-journal/tests/scale.scenario.json +286 -0
  205. package/dist/content/examples/playervox-journal/tests/signed-out.scenario.json +134 -0
  206. package/dist/content/examples/playervox-journal/tests/states.scenario.json +741 -0
  207. package/dist/content/examples/playervox-journal/tests/unavailable.scenario.json +183 -0
  208. package/dist/content/examples/playervox-journal/tsconfig.json +16 -0
  209. package/dist/content/examples/playervox-journal/view.ocml +52 -0
  210. package/dist/content/examples/playervox-rating/LICENSE +21 -0
  211. package/dist/content/examples/playervox-rating/README.md +166 -0
  212. package/dist/content/examples/playervox-rating/listing.json +19 -0
  213. package/dist/content/examples/playervox-rating/locales/en.json +35 -0
  214. package/dist/content/examples/playervox-rating/locales/fr.json +35 -0
  215. package/dist/content/examples/playervox-rating/logic.ts +581 -0
  216. package/dist/content/examples/playervox-rating/manifest.json +19 -0
  217. package/dist/content/examples/playervox-rating/package.json +16 -0
  218. package/dist/content/examples/playervox-rating/style.ocss +333 -0
  219. package/dist/content/examples/playervox-rating/tests/account.scenario.json +125 -0
  220. package/dist/content/examples/playervox-rating/tests/bounds.scenario.json +73 -0
  221. package/dist/content/examples/playervox-rating/tests/draft.scenario.json +199 -0
  222. package/dist/content/examples/playervox-rating/tests/form.scenario.json +142 -0
  223. package/dist/content/examples/playervox-rating/tests/load-logic.mjs +22 -0
  224. package/dist/content/examples/playervox-rating/tests/logic.test.mjs +346 -0
  225. package/dist/content/examples/playervox-rating/tests/offline.scenario.json +142 -0
  226. package/dist/content/examples/playervox-rating/tests/preview.scenario.json +44 -0
  227. package/dist/content/examples/playervox-rating/tests/publish-keyboard.scenario.json +221 -0
  228. package/dist/content/examples/playervox-rating/tests/publish-pointer.scenario.json +314 -0
  229. package/dist/content/examples/playervox-rating/tests/read-only.scenario.json +70 -0
  230. package/dist/content/examples/playervox-rating/tests/refused.scenario.json +45 -0
  231. package/dist/content/examples/playervox-rating/tests/states.scenario.json +362 -0
  232. package/dist/content/examples/playervox-rating/tsconfig.json +16 -0
  233. package/dist/content/examples/playervox-rating/view.ocml +62 -0
  234. package/dist/content/examples/playervox-reviews/LICENSE +21 -0
  235. package/dist/content/examples/playervox-reviews/README.md +194 -0
  236. package/dist/content/examples/playervox-reviews/listing.json +19 -0
  237. package/dist/content/examples/playervox-reviews/locales/en.json +32 -0
  238. package/dist/content/examples/playervox-reviews/locales/fr.json +32 -0
  239. package/dist/content/examples/playervox-reviews/logic.ts +822 -0
  240. package/dist/content/examples/playervox-reviews/manifest.json +30 -0
  241. package/dist/content/examples/playervox-reviews/package.json +16 -0
  242. package/dist/content/examples/playervox-reviews/style.ocss +260 -0
  243. package/dist/content/examples/playervox-reviews/tests/account.scenario.json +249 -0
  244. package/dist/content/examples/playervox-reviews/tests/bounds.scenario.json +336 -0
  245. package/dist/content/examples/playervox-reviews/tests/changes.scenario.json +515 -0
  246. package/dist/content/examples/playervox-reviews/tests/filter.scenario.json +400 -0
  247. package/dist/content/examples/playervox-reviews/tests/frame.scenario.json +185 -0
  248. package/dist/content/examples/playervox-reviews/tests/keyboard.scenario.json +188 -0
  249. package/dist/content/examples/playervox-reviews/tests/load-logic.mjs +22 -0
  250. package/dist/content/examples/playervox-reviews/tests/logic.test.mjs +559 -0
  251. package/dist/content/examples/playervox-reviews/tests/offline-start.scenario.json +152 -0
  252. package/dist/content/examples/playervox-reviews/tests/offline.scenario.json +311 -0
  253. package/dist/content/examples/playervox-reviews/tests/paging.scenario.json +497 -0
  254. package/dist/content/examples/playervox-reviews/tests/preview.scenario.json +286 -0
  255. package/dist/content/examples/playervox-reviews/tests/reading.scenario.json +591 -0
  256. package/dist/content/examples/playervox-reviews/tests/refused.scenario.json +54 -0
  257. package/dist/content/examples/playervox-reviews/tests/scale.scenario.json +303 -0
  258. package/dist/content/examples/playervox-reviews/tests/states.scenario.json +700 -0
  259. package/dist/content/examples/playervox-reviews/tests/unavailable.scenario.json +192 -0
  260. package/dist/content/examples/playervox-reviews/tsconfig.json +16 -0
  261. package/dist/content/examples/playervox-reviews/view.ocml +110 -0
  262. package/dist/content/examples/playervox-score/LICENSE +21 -0
  263. package/dist/content/examples/playervox-score/README.md +131 -0
  264. package/dist/content/examples/playervox-score/listing.json +19 -0
  265. package/dist/content/examples/playervox-score/locales/en.json +23 -0
  266. package/dist/content/examples/playervox-score/locales/fr.json +23 -0
  267. package/dist/content/examples/playervox-score/logic.ts +430 -0
  268. package/dist/content/examples/playervox-score/manifest.json +47 -0
  269. package/dist/content/examples/playervox-score/package.json +16 -0
  270. package/dist/content/examples/playervox-score/style.ocss +247 -0
  271. package/dist/content/examples/playervox-score/tests/grades.scenario.json +279 -0
  272. package/dist/content/examples/playervox-score/tests/load-logic.mjs +22 -0
  273. package/dist/content/examples/playervox-score/tests/logic.test.mjs +229 -0
  274. package/dist/content/examples/playervox-score/tests/options.scenario.json +302 -0
  275. package/dist/content/examples/playervox-score/tests/preview.scenario.json +68 -0
  276. package/dist/content/examples/playervox-score/tests/pulse.scenario.json +157 -0
  277. package/dist/content/examples/playervox-score/tests/refused.scenario.json +21 -0
  278. package/dist/content/examples/playervox-score/tests/signed-out.scenario.json +46 -0
  279. package/dist/content/examples/playervox-score/tests/states.scenario.json +450 -0
  280. package/dist/content/examples/playervox-score/tests/unavailable.scenario.json +84 -0
  281. package/dist/content/examples/playervox-score/tsconfig.json +16 -0
  282. package/dist/content/examples/playervox-score/view.ocml +51 -0
  283. package/dist/content/examples/session/LICENSE +21 -0
  284. package/dist/content/examples/session/README.md +57 -0
  285. package/dist/content/examples/session/listing.json +19 -0
  286. package/dist/content/examples/session/locales/en.json +3 -0
  287. package/dist/content/examples/session/locales/fr.json +3 -0
  288. package/dist/content/examples/session/logic.ts +26 -0
  289. package/dist/content/examples/session/manifest.json +19 -0
  290. package/dist/content/examples/session/package.json +16 -0
  291. package/dist/content/examples/session/style.ocss +16 -0
  292. package/dist/content/examples/session/tests/hidden.scenario.json +45 -0
  293. package/dist/content/examples/session/tests/load-logic.mjs +22 -0
  294. package/dist/content/examples/session/tests/logic.test.mjs +37 -0
  295. package/dist/content/examples/session/tests/preview.scenario.json +28 -0
  296. package/dist/content/examples/session/tests/refused.scenario.json +20 -0
  297. package/dist/content/examples/session/tests/refused.test.mjs +16 -0
  298. package/dist/content/examples/session/tests/render.scenario.json +95 -0
  299. package/dist/content/examples/session/tests/ticks.scenario.json +139 -0
  300. package/dist/content/examples/session/tests/unknown.scenario.json +37 -0
  301. package/dist/content/examples/session/tsconfig.json +16 -0
  302. package/dist/content/examples/session/view.ocml +11 -0
  303. package/dist/content/examples/stopwatch/LICENSE +21 -0
  304. package/dist/content/examples/stopwatch/README.md +99 -0
  305. package/dist/content/examples/stopwatch/listing.json +19 -0
  306. package/dist/content/examples/stopwatch/locales/en.json +8 -0
  307. package/dist/content/examples/stopwatch/locales/fr.json +8 -0
  308. package/dist/content/examples/stopwatch/logic.ts +93 -0
  309. package/dist/content/examples/stopwatch/manifest.json +19 -0
  310. package/dist/content/examples/stopwatch/package.json +16 -0
  311. package/dist/content/examples/stopwatch/style.ocss +54 -0
  312. package/dist/content/examples/stopwatch/tests/errors.scenario.json +121 -0
  313. package/dist/content/examples/stopwatch/tests/gesture.scenario.json +154 -0
  314. package/dist/content/examples/stopwatch/tests/hidden.scenario.json +57 -0
  315. package/dist/content/examples/stopwatch/tests/hundredths.scenario.json +173 -0
  316. package/dist/content/examples/stopwatch/tests/load-logic.mjs +22 -0
  317. package/dist/content/examples/stopwatch/tests/logic.test.mjs +99 -0
  318. package/dist/content/examples/stopwatch/tests/preview.scenario.json +36 -0
  319. package/dist/content/examples/stopwatch/tests/read-only.scenario.json +42 -0
  320. package/dist/content/examples/stopwatch/tests/read-only.test.mjs +18 -0
  321. package/dist/content/examples/stopwatch/tests/refused.scenario.json +22 -0
  322. package/dist/content/examples/stopwatch/tests/refused.test.mjs +18 -0
  323. package/dist/content/examples/stopwatch/tests/render.scenario.json +105 -0
  324. package/dist/content/examples/stopwatch/tests/states.scenario.json +246 -0
  325. package/dist/content/examples/stopwatch/tests/unknown.scenario.json +40 -0
  326. package/dist/content/examples/stopwatch/tsconfig.json +16 -0
  327. package/dist/content/examples/stopwatch/view.ocml +29 -0
  328. package/dist/content/examples/twitch-chat/LICENSE +21 -0
  329. package/dist/content/examples/twitch-chat/README.md +283 -0
  330. package/dist/content/examples/twitch-chat/listing.json +19 -0
  331. package/dist/content/examples/twitch-chat/locales/en.json +52 -0
  332. package/dist/content/examples/twitch-chat/locales/fr.json +52 -0
  333. package/dist/content/examples/twitch-chat/logic.ts +887 -0
  334. package/dist/content/examples/twitch-chat/manifest.json +33 -0
  335. package/dist/content/examples/twitch-chat/package.json +16 -0
  336. package/dist/content/examples/twitch-chat/style.ocss +544 -0
  337. package/dist/content/examples/twitch-chat/tests/account.scenario.json +325 -0
  338. package/dist/content/examples/twitch-chat/tests/burst.scenario.json +1866 -0
  339. package/dist/content/examples/twitch-chat/tests/channel-refused.scenario.json +372 -0
  340. package/dist/content/examples/twitch-chat/tests/channel.scenario.json +480 -0
  341. package/dist/content/examples/twitch-chat/tests/draft.scenario.json +279 -0
  342. package/dist/content/examples/twitch-chat/tests/fade.scenario.json +269 -0
  343. package/dist/content/examples/twitch-chat/tests/failures.scenario.json +420 -0
  344. package/dist/content/examples/twitch-chat/tests/favorites.scenario.json +376 -0
  345. package/dist/content/examples/twitch-chat/tests/frame.scenario.json +152 -0
  346. package/dist/content/examples/twitch-chat/tests/lifetime.scenario.json +186 -0
  347. package/dist/content/examples/twitch-chat/tests/load-logic.mjs +22 -0
  348. package/dist/content/examples/twitch-chat/tests/logic.test.mjs +1053 -0
  349. package/dist/content/examples/twitch-chat/tests/passive.scenario.json +536 -0
  350. package/dist/content/examples/twitch-chat/tests/preview.scenario.json +147 -0
  351. package/dist/content/examples/twitch-chat/tests/read-only.scenario.json +202 -0
  352. package/dist/content/examples/twitch-chat/tests/read-only.test.mjs +63 -0
  353. package/dist/content/examples/twitch-chat/tests/refused.scenario.json +65 -0
  354. package/dist/content/examples/twitch-chat/tests/refused.test.mjs +43 -0
  355. package/dist/content/examples/twitch-chat/tests/render.scenario.json +347 -0
  356. package/dist/content/examples/twitch-chat/tests/reply.scenario.json +384 -0
  357. package/dist/content/examples/twitch-chat/tests/send-refused.scenario.json +383 -0
  358. package/dist/content/examples/twitch-chat/tests/send.scenario.json +300 -0
  359. package/dist/content/examples/twitch-chat/tests/states.scenario.json +455 -0
  360. package/dist/content/examples/twitch-chat/tests/unavailable.scenario.json +256 -0
  361. package/dist/content/examples/twitch-chat/tests/unread.scenario.json +997 -0
  362. package/dist/content/examples/twitch-chat/tsconfig.json +16 -0
  363. package/dist/content/examples/twitch-chat/view.ocml +141 -0
  364. package/dist/content/examples/warframe-market/LICENSE +21 -0
  365. package/dist/content/examples/warframe-market/README.md +176 -0
  366. package/dist/content/examples/warframe-market/listing.json +19 -0
  367. package/dist/content/examples/warframe-market/locales/en.json +44 -0
  368. package/dist/content/examples/warframe-market/locales/fr.json +44 -0
  369. package/dist/content/examples/warframe-market/logic.ts +1265 -0
  370. package/dist/content/examples/warframe-market/manifest.json +92 -0
  371. package/dist/content/examples/warframe-market/package.json +16 -0
  372. package/dist/content/examples/warframe-market/style.ocss +422 -0
  373. package/dist/content/examples/warframe-market/tests/bounds.scenario.json +60 -0
  374. package/dist/content/examples/warframe-market/tests/cached.scenario.json +70 -0
  375. package/dist/content/examples/warframe-market/tests/detail.scenario.json +251 -0
  376. package/dist/content/examples/warframe-market/tests/errors.scenario.json +105 -0
  377. package/dist/content/examples/warframe-market/tests/fixtures.json +295 -0
  378. package/dist/content/examples/warframe-market/tests/load-logic.mjs +22 -0
  379. package/dist/content/examples/warframe-market/tests/logic.test.mjs +505 -0
  380. package/dist/content/examples/warframe-market/tests/no-clipboard.scenario.json +104 -0
  381. package/dist/content/examples/warframe-market/tests/passive.scenario.json +87 -0
  382. package/dist/content/examples/warframe-market/tests/preview.scenario.json +146 -0
  383. package/dist/content/examples/warframe-market/tests/refresh.scenario.json +56 -0
  384. package/dist/content/examples/warframe-market/tests/refused.scenario.json +74 -0
  385. package/dist/content/examples/warframe-market/tests/render.scenario.json +210 -0
  386. package/dist/content/examples/warframe-market/tests/search.scenario.json +189 -0
  387. package/dist/content/examples/warframe-market/tests/unavailable.scenario.json +54 -0
  388. package/dist/content/examples/warframe-market/tsconfig.json +16 -0
  389. package/dist/content/examples/warframe-market/view.ocml +116 -0
  390. package/dist/content/examples/weather/LICENSE +20 -0
  391. package/dist/content/examples/weather/listing.json +10 -0
  392. package/dist/content/examples/weather/locales/en.json +9 -0
  393. package/dist/content/examples/weather/locales/fr.json +9 -0
  394. package/dist/content/examples/weather/logic.ts +62 -0
  395. package/dist/content/examples/weather/manifest.json +51 -0
  396. package/dist/content/examples/weather/package.json +15 -0
  397. package/dist/content/examples/weather/style.ocss +29 -0
  398. package/dist/content/examples/weather/tsconfig.json +16 -0
  399. package/dist/content/examples/weather/view.ocml +12 -0
  400. package/dist/content/index.json +3601 -0
  401. package/dist/content/reference/errors.json +1712 -0
  402. package/dist/content/reference/limits.json +1154 -0
  403. package/dist/content/reference/manifest.md +223 -0
  404. package/dist/content/reference/permissions.json +413 -0
  405. package/dist/content/templates/blank/locales/en.json +3 -0
  406. package/dist/content/templates/blank/locales/fr.json +3 -0
  407. package/dist/content/templates/blank/logic.ts +7 -0
  408. package/dist/content/templates/blank/manifest.json +16 -0
  409. package/dist/content/templates/blank/style.ocss +10 -0
  410. package/dist/content/templates/blank/tests/example.scenario.json +10 -0
  411. package/dist/content/templates/blank/view.ocml +6 -0
  412. package/dist/content/templates/chart/locales/en.json +3 -0
  413. package/dist/content/templates/chart/locales/fr.json +3 -0
  414. package/dist/content/templates/chart/logic.ts +27 -0
  415. package/dist/content/templates/chart/manifest.json +16 -0
  416. package/dist/content/templates/chart/style.ocss +27 -0
  417. package/dist/content/templates/chart/tests/example.scenario.json +10 -0
  418. package/dist/content/templates/chart/view.ocml +10 -0
  419. package/dist/content/templates/counter/locales/en.json +4 -0
  420. package/dist/content/templates/counter/locales/fr.json +4 -0
  421. package/dist/content/templates/counter/logic.ts +20 -0
  422. package/dist/content/templates/counter/manifest.json +16 -0
  423. package/dist/content/templates/counter/style.ocss +26 -0
  424. package/dist/content/templates/counter/tests/example.scenario.json +11 -0
  425. package/dist/content/templates/counter/view.ocml +13 -0
  426. package/dist/content/templates/list/locales/en.json +8 -0
  427. package/dist/content/templates/list/locales/fr.json +8 -0
  428. package/dist/content/templates/list/logic.ts +35 -0
  429. package/dist/content/templates/list/manifest.json +16 -0
  430. package/dist/content/templates/list/style.ocss +42 -0
  431. package/dist/content/templates/list/tests/example.scenario.json +10 -0
  432. package/dist/content/templates/list/view.ocml +17 -0
  433. package/dist/content/templates/shared/LICENSE +20 -0
  434. package/dist/content/templates/shared/package.json +14 -0
  435. package/dist/content/templates/shared/tsconfig.json +16 -0
  436. package/dist/content.js +68 -0
  437. package/dist/env.js +156 -0
  438. package/dist/fingerprint.js +32 -0
  439. package/dist/ids.js +27 -0
  440. package/dist/index.js +82 -0
  441. package/dist/knowledge.js +222 -0
  442. package/dist/npm.js +149 -0
  443. package/dist/prompts.js +47 -0
  444. package/dist/resources.js +82 -0
  445. package/dist/run.js +120 -0
  446. package/dist/server.js +43 -0
  447. package/dist/session.js +205 -0
  448. package/dist/sources.js +218 -0
  449. package/dist/text.js +80 -0
  450. package/dist/texts.js +44 -0
  451. package/dist/tools/audit.js +105 -0
  452. package/dist/tools/build.js +453 -0
  453. package/dist/tools/common.js +122 -0
  454. package/dist/tools/docs.js +196 -0
  455. package/dist/tools/preview.js +87 -0
  456. package/dist/tools/project.js +178 -0
  457. package/dist/tools/setup.js +146 -0
  458. package/dist/tools/submission.js +369 -0
  459. package/dist/version.js +3 -0
  460. package/npm-shrinkwrap.json +402 -0
  461. package/package.json +52 -4
@@ -0,0 +1,822 @@
1
+ // Player reviews: what PlayerVox players think of the active game, three
2
+ // reviews per page, optionally from the players the user follows. OverCrow
3
+ // reads the reviews with the user's PlayerVox session, in the language of
4
+ // its interface, and owns the account's sign-in; the widget follows the
5
+ // reviews' revision, reads its own page with its own filter and lays it
6
+ // out.
7
+ import {
8
+ draw,
9
+ formatDate,
10
+ formatNumber,
11
+ hasGrant,
12
+ host,
13
+ initState,
14
+ onHost,
15
+ option,
16
+ playervox,
17
+ t,
18
+ timers,
19
+ type Color,
20
+ type DrawCommand,
21
+ type Review,
22
+ type ReviewsPage,
23
+ type ServiceError,
24
+ type Subscription,
25
+ type Timer,
26
+ } from "@overcrow/sdk";
27
+
28
+ /** What the widget shows in place of the reviews. */
29
+ export type Phase = "loading" | "idle" | "unsupported" | "ready" | "failed";
30
+
31
+ /** How the user reads one review of the page shown. */
32
+ export interface Reading {
33
+ /** The whole text, past the three-line fold. */
34
+ expanded: boolean;
35
+ /** The untranslated text of a translated review. */
36
+ original: boolean;
37
+ /** A review the community hid, shown at the user's request. */
38
+ revealed: boolean;
39
+ }
40
+
41
+ declare module "@overcrow/sdk" {
42
+ interface WidgetState {
43
+ /**
44
+ * `loading` until the first page, `idle` without an active game,
45
+ * `unsupported` for a game without a Steam app ID, `ready` with a
46
+ * page, `failed` without one after a failure.
47
+ */
48
+ phase: Phase;
49
+ /** The page shown; `null` until the first one. */
50
+ page: ReviewsPage | null;
51
+ /** The filter the page shown was read with. */
52
+ followedOnly: boolean;
53
+ /** Reading choices of the page shown, by review ID. */
54
+ reading: Record<string, Reading>;
55
+ /** Message key of the last failed read; `""` for none. */
56
+ problem: string;
57
+ /** PlayerVox is unreachable: nothing is read, the page shown stays. */
58
+ offline: boolean;
59
+ /** A read is in flight: the paging buttons wait. */
60
+ busy: boolean;
61
+ interactive: boolean;
62
+ }
63
+ }
64
+
65
+ /**
66
+ * After the host ends the subscription with a failure, the widget
67
+ * subscribes again this much later.
68
+ */
69
+ export const RETRY_MS = 5_000;
70
+
71
+ /** A read answered `busy` or `stale_context` is asked again this much later. */
72
+ export const REREAD_MS = 1_000;
73
+
74
+ /** Consecutive re-reads after `busy` or `stale_context`; the next revision reads again. */
75
+ export const MAX_REREADS = 3;
76
+
77
+ /** Reviews per page, as PlayerVox pages them: one badge canvas each. */
78
+ export const PAGE_SIZE = 3;
79
+
80
+ /**
81
+ * A review longer than this, or of more than three lines, folds to three
82
+ * lines behind "Read more": about three lines of the narrowest panel. The
83
+ * host does not tell whether a text is cut, so a text close to the bound
84
+ * may get the button without being cut.
85
+ */
86
+ export const FOLD_CHARACTERS = 120;
87
+
88
+ /**
89
+ * Characters of a folded review given to the host. Its three lines hold
90
+ * fewer on the widest panel (about 140 per line at 900 px), and the host
91
+ * lays out every character it is given, shown or not: an 8000-character
92
+ * review folded costs the host as much as unfolded otherwise.
93
+ */
94
+ export const FOLDED_CHARACTERS = 450;
95
+
96
+ /** `label` and `tooltip` bound (`MAX_LABEL_BYTES`). */
97
+ const MAX_LABEL_BYTES = 256;
98
+
99
+ // <shared:playervox-badge/badge.ts>
100
+ // The PlayerVox grade badge and mark, shared by the Score and Rating
101
+ // built-ins; the Journal draws the mark alone, and Player reviews the mark
102
+ // and a smaller badge per review. scripts/sync-shared-widgets.mjs
103
+ // copies this file, as is, into
104
+ // their logic.ts between the markers `// <shared:playervox-badge/badge.ts>`;
105
+ // edit it here, never in a copy. The copy relies on the widget importing
106
+ // the `Color` and `DrawCommand` types from @overcrow/sdk.
107
+
108
+ /** A PlayerVox grade, or `--` without a valid score. */
109
+ export type BadgeGrade = "S+" | "S" | "A" | "B" | "C" | "D" | "F" | "--";
110
+
111
+ /**
112
+ * The grade of a 0–100 value with PlayerVox's thresholds, on the raw value
113
+ * (89.6 is S, although it shows as 90); `--` for none or out of range. The
114
+ * host grades the overall score itself; widgets grade the criteria.
115
+ */
116
+ export function gradeOf(value: number | null): BadgeGrade {
117
+ if (value === null || !Number.isFinite(value) || value < 0 || value > 100) {
118
+ return "--";
119
+ }
120
+ if (value >= 90) return "S+";
121
+ if (value >= 80) return "S";
122
+ if (value >= 70) return "A";
123
+ if (value >= 60) return "B";
124
+ if (value >= 40) return "C";
125
+ if (value >= 20) return "D";
126
+ return "F";
127
+ }
128
+
129
+ /**
130
+ * The grade colours of PlayerVox's site (its Tailwind palette in sRGB), in
131
+ * both themes: the badge keeps its black face on a light panel too.
132
+ */
133
+ export const GRADE_COLORS: Readonly<Record<BadgeGrade, Color>> = {
134
+ "S+": "#9ae600",
135
+ S: "#7ccf00",
136
+ A: "#00d3f3",
137
+ B: "#fdc700",
138
+ C: "#fe9a00",
139
+ D: "#e7000b",
140
+ F: "#52525c",
141
+ "--": "#52525c",
142
+ };
143
+
144
+ /** The style class of a grade's colour (`badge.ocss`): `grade-s-plus`… */
145
+ export function gradeClass(grade: BadgeGrade): string {
146
+ return grade === "--" ? "grade-none" : `grade-${grade.toLowerCase().replace("+", "-plus")}`;
147
+ }
148
+
149
+ /**
150
+ * Side of the badge canvas in logical px: the 56 px badge turned by 4°,
151
+ * its 3 px border and the S+ glow, centred.
152
+ */
153
+ export const BADGE_CANVAS = 76;
154
+
155
+ const BADGE_HALF = 28;
156
+ const BADGE_RADIUS = 12;
157
+ /** The letter's baseline below the centre: the capitals sit centred. */
158
+ const LETTER_BASELINE = 9;
159
+
160
+ /**
161
+ * The neutral `--` badge per theme: a translucent face and a grey border
162
+ * and text that keep their contrast on the panel. A graded badge keeps its
163
+ * black face in both themes.
164
+ */
165
+ const NEUTRAL: Readonly<Record<"dark" | "light", { face: Color; color: Color }>> = {
166
+ dark: { face: "#27272a80", color: "#52525c" },
167
+ light: { face: "#e4e4e780", color: "#71717a" },
168
+ };
169
+
170
+ /**
171
+ * The badge of a grade: a rounded square with a black face and a border
172
+ * of the grade's colour, turned by 4° with its letter (not `--`, which is
173
+ * neutral and square), and a faint static glow for S+.
174
+ */
175
+ export function badgeCommands(grade: BadgeGrade, theme: "dark" | "light"): DrawCommand[] {
176
+ const neutral = grade === "--" ? NEUTRAL[theme] : null;
177
+ const color = neutral?.color ?? GRADE_COLORS[grade];
178
+ const centre = BADGE_CANVAS / 2;
179
+ const square = (): DrawCommand => [
180
+ "rect",
181
+ -BADGE_HALF,
182
+ -BADGE_HALF,
183
+ BADGE_HALF * 2,
184
+ BADGE_HALF * 2,
185
+ BADGE_RADIUS,
186
+ ];
187
+ const commands: DrawCommand[] = [
188
+ ["save"],
189
+ ["translate", centre, centre],
190
+ ["rotate", grade === "--" ? 0 : 4],
191
+ ];
192
+ if (grade === "S+") {
193
+ // Eight widening rings of the accent at 2 % each: a soft halo.
194
+ for (let ring = 8; ring >= 1; ring -= 1) {
195
+ commands.push(square(), ["stroke", "#a3e63505", 3 + ring * 1.5]);
196
+ }
197
+ }
198
+ commands.push(
199
+ square(),
200
+ ["fill", neutral?.face ?? "#000000"],
201
+ square(),
202
+ ["stroke", color, 3],
203
+ ["text", 0, LETTER_BASELINE, grade, 26, color, "center", "display", "700"],
204
+ ["restore"],
205
+ );
206
+ return commands;
207
+ }
208
+
209
+ /**
210
+ * The badge's pulse class after `current` (`badge.ocss`): it alternates, so
211
+ * that each new score restarts the host's animation.
212
+ */
213
+ export function nextPulse(current: string): "pulse-a" | "pulse-b" {
214
+ return current === "pulse-a" ? "pulse-b" : "pulse-a";
215
+ }
216
+
217
+ /** The accessible name of a badge: "Grade S+, 94/100". */
218
+ export function badgeLabel(grade: BadgeGrade, score: number | null, words: { grade: string; none: string }): string {
219
+ if (grade === "--" || score === null) {
220
+ return words.none;
221
+ }
222
+ return `${words.grade} ${grade}, ${roundHalfAway(score)}/100`;
223
+ }
224
+
225
+ /** Rounds half away from zero: 72.5 is 73 (parity sheet decision 6). */
226
+ export function roundHalfAway(value: number): number {
227
+ return Math.sign(value) * Math.round(Math.abs(value));
228
+ }
229
+
230
+ // The PlayerVox mark: three rising bars, in unit coordinates of its box.
231
+ const MARK_BARS: readonly (readonly number[])[] = [
232
+ [0.09403, 0.29375, 0.04806, 0.3068, 0.02098, 0.34, 0.01785, 0.38207, 0.01785, 0.42385, 0.01785, 0.46563, 0.01785, 0.50742, 0.01785, 0.5492, 0.01785, 0.59098, 0.01785, 0.63276, 0.01785, 0.67454, 0.01785, 0.71632, 0.01785, 0.75811, 0.01785, 0.79989, 0.01785, 0.84167, 0.01785, 0.88345, 0.01785, 0.92523, 0.01785, 0.96701, 0.04115, 0.96149, 0.07617, 0.93218, 0.11119, 0.90286, 0.14621, 0.87354, 0.18123, 0.84422, 0.21625, 0.8149, 0.25127, 0.78558, 0.28629, 0.75626, 0.31138, 0.72341, 0.31138, 0.68162, 0.31138, 0.63984, 0.31138, 0.59806, 0.31138, 0.55628, 0.31138, 0.5145, 0.31138, 0.47272, 0.31138, 0.43094, 0.31138, 0.38915, 0.30981, 0.34705, 0.28603, 0.31194, 0.24179, 0.29574, 0.19233, 0.29497, 0.14318, 0.29436],
233
+ [0.43235, 0.1525, 0.38372, 0.16747, 0.35807, 0.20475, 0.35676, 0.24975, 0.35676, 0.29442, 0.35676, 0.33908, 0.35676, 0.38375, 0.35676, 0.42842, 0.35676, 0.47308, 0.35676, 0.51775, 0.35676, 0.56242, 0.35676, 0.60708, 0.35676, 0.65175, 0.35676, 0.69642, 0.3648, 0.73425, 0.41735, 0.73425, 0.4699, 0.73425, 0.52245, 0.73425, 0.575, 0.73425, 0.62755, 0.73425, 0.65059, 0.70917, 0.65059, 0.6645, 0.65059, 0.61983, 0.65059, 0.57517, 0.65059, 0.5305, 0.65059, 0.48583, 0.65059, 0.44117, 0.65059, 0.3965, 0.65059, 0.35183, 0.65059, 0.30717, 0.65059, 0.2625, 0.65059, 0.21783, 0.63377, 0.17623, 0.59039, 0.15382, 0.53745, 0.1525, 0.4849, 0.1525],
234
+ [0.76765, 0.01375, 0.71253, 0.03393, 0.69206, 0.08189, 0.69206, 0.13433, 0.69206, 0.18678, 0.69206, 0.23922, 0.69206, 0.29167, 0.69206, 0.34411, 0.69206, 0.39656, 0.69206, 0.449, 0.69206, 0.50144, 0.69206, 0.55389, 0.69206, 0.60633, 0.69206, 0.65878, 0.69206, 0.71122, 0.7252, 0.7355, 0.7869, 0.7355, 0.84859, 0.7355, 0.91029, 0.7355, 0.97199, 0.7355, 0.98588, 0.69486, 0.98588, 0.64242, 0.98588, 0.58997, 0.98588, 0.53753, 0.98588, 0.48508, 0.98588, 0.43264, 0.98588, 0.38019, 0.98588, 0.32775, 0.98588, 0.27531, 0.98588, 0.22286, 0.98588, 0.17042, 0.98588, 0.11797, 0.98438, 0.06513, 0.95084, 0.02369, 0.89105, 0.01375, 0.82935, 0.01375],
235
+ ];
236
+
237
+ /**
238
+ * The PlayerVox mark, `height` px tall and 0.85 as wide, filled with
239
+ * `color` and centred in a canvas `canvasWidth` px wide: give the canvas a
240
+ * whole number of pixels, so that its texture is never resampled.
241
+ */
242
+ export function markCommands(canvasWidth: number, height: number, color: Color): DrawCommand[] {
243
+ const width = height * 0.85;
244
+ const left = (canvasWidth - width) / 2;
245
+ const commands: DrawCommand[] = [];
246
+ for (const bar of MARK_BARS) {
247
+ for (let index = 0; index + 1 < bar.length; index += 2) {
248
+ const x = left + (bar[index] ?? 0) * width;
249
+ const y = (bar[index + 1] ?? 0) * height;
250
+ commands.push(index === 0 ? ["moveTo", x, y] : ["lineTo", x, y]);
251
+ }
252
+ commands.push(["close"], ["fill", color]);
253
+ }
254
+ return commands;
255
+ }
256
+ // </shared:playervox-badge/badge.ts>
257
+
258
+ const canRead = hasGrant("playervox.reviews.read");
259
+
260
+ const state = initState({
261
+ phase: (canRead ? "loading" : "failed") as Phase,
262
+ page: null as ReviewsPage | null,
263
+ followedOnly: option("followed-only", false),
264
+ reading: {} as Record<string, Reading>,
265
+ problem: canRead ? "" : "no-permission",
266
+ offline: false,
267
+ busy: false,
268
+ interactive: host.mode === "interactive",
269
+ });
270
+
271
+ /** Provider text on one line. */
272
+ export function oneLine(text: string): string {
273
+ return text.replace(/\s*[\r\n]+\s*/g, " ").trim();
274
+ }
275
+
276
+ /** `text` cut to `limit` UTF-8 bytes on a character boundary. */
277
+ export function cutBytes(text: string, limit: number): string {
278
+ let bytes = 0;
279
+ let end = 0;
280
+ for (const character of text) {
281
+ const code = character.codePointAt(0) ?? 0;
282
+ const size = code < 0x80 ? 1 : code < 0x800 ? 2 : code < 0x10000 ? 3 : 4;
283
+ if (bytes + size > limit) {
284
+ return text.slice(0, end);
285
+ }
286
+ bytes += size;
287
+ end += character.length;
288
+ }
289
+ return text;
290
+ }
291
+
292
+ /** The game's name, when PlayerVox gave one. */
293
+ export function gameName(page: ReviewsPage | null): string {
294
+ return page === null ? "" : oneLine(page.gameName);
295
+ }
296
+
297
+ /**
298
+ * A review's date in the local time it carries and the user's date order
299
+ * ("09/20/2026", FR "20/09/2026"); an em dash for a timestamp outside the
300
+ * calendar.
301
+ */
302
+ export function dateText(review: Review): string {
303
+ if (!Number.isFinite(review.publishedAt) || review.publishedAt < 0) {
304
+ return "—";
305
+ }
306
+ return formatDate(review.publishedAt, { offsetMinutes: review.offsetMinutes });
307
+ }
308
+
309
+ /** A translated review carries its untranslated text. */
310
+ export function isTranslated(review: Review): boolean {
311
+ return review.original !== null && review.original.trim() !== "";
312
+ }
313
+
314
+ /** The text a reading shows: the translation, or the original on request. */
315
+ export function shownText(review: Review, reading: Reading): string {
316
+ const text = reading.original && isTranslated(review) ? review.original : review.text;
317
+ return (text ?? "").trim();
318
+ }
319
+
320
+ /** Whether `text` folds: longer than three lines of the narrowest panel. */
321
+ export function folds(text: string): boolean {
322
+ let characters = 0;
323
+ let lines = 1;
324
+ for (const character of text) {
325
+ characters += 1;
326
+ if (character === "\n") {
327
+ lines += 1;
328
+ }
329
+ if (characters > FOLD_CHARACTERS || lines > 3) {
330
+ return true;
331
+ }
332
+ }
333
+ return false;
334
+ }
335
+
336
+ /**
337
+ * The start of a folded review: `text` within `FOLDED_CHARACTERS`, with an
338
+ * ellipsis when it was cut (the host adds its own where the third line
339
+ * ends).
340
+ */
341
+ export function foldedText(text: string): string {
342
+ let characters = 0;
343
+ let end = 0;
344
+ for (const character of text) {
345
+ if (characters === FOLDED_CHARACTERS) {
346
+ return `${text.slice(0, graphemeStart(text, end))}…`;
347
+ }
348
+ characters += 1;
349
+ end += character.length;
350
+ }
351
+ return text;
352
+ }
353
+
354
+ /** What continues the character before it: a combining mark, a variation selector, a joiner, a skin tone, a tag. */
355
+ const CONTINUES = /^[\p{M}\u200d\ufe00-\ufe0f\u{1f3fb}-\u{1f3ff}\u{e0020}-\u{e007f}]/u;
356
+ /** The two letters of a flag. */
357
+ const FLAG_LETTER = /^[\u{1f1e6}-\u{1f1ff}]/u;
358
+
359
+ /**
360
+ * The cut at or before `end` (an index between two characters of `text`)
361
+ * that leaves every grapheme whole: an accented letter keeps its
362
+ * combining accent, an emoji sequence its joined parts and a flag its two
363
+ * letters.
364
+ */
365
+ export function graphemeStart(text: string, end: number): number {
366
+ let cut = end;
367
+ while (cut > 0 && insideGrapheme(text, cut)) {
368
+ cut = before(text, cut);
369
+ }
370
+ return cut;
371
+ }
372
+
373
+ /** The index of the character that ends at `index`. */
374
+ function before(text: string, index: number): number {
375
+ const low = text.charCodeAt(index - 1);
376
+ const high = index > 1 ? text.charCodeAt(index - 2) : 0;
377
+ return low >= 0xdc00 && low <= 0xdfff && high >= 0xd800 && high <= 0xdbff ? index - 2 : index - 1;
378
+ }
379
+
380
+ function insideGrapheme(text: string, index: number): boolean {
381
+ const next = text.slice(index, index + 2);
382
+ if (next === "") {
383
+ return false;
384
+ }
385
+ if (CONTINUES.test(next) || text.charCodeAt(index - 1) === 0x200d) {
386
+ return true;
387
+ }
388
+ if (!FLAG_LETTER.test(next)) {
389
+ return false;
390
+ }
391
+ // Flag letters pair up from the first of their run: a cut after an odd
392
+ // number of them falls inside a flag.
393
+ let letters = 0;
394
+ for (let at = index; at > 0; at = before(text, at)) {
395
+ if (!FLAG_LETTER.test(text.slice(before(text, at), at))) {
396
+ break;
397
+ }
398
+ letters += 1;
399
+ }
400
+ return letters % 2 === 1;
401
+ }
402
+
403
+ const UNREAD: Reading = { expanded: false, original: false, revealed: false };
404
+
405
+ /** One review as the view lays it out; every member is ready to show. */
406
+ export interface Row {
407
+ id: string;
408
+ author: string;
409
+ /** The whole name, for a name the panel cuts. */
410
+ authorTip: string;
411
+ date: string;
412
+ score: string;
413
+ /** Accessible name of the badge: "Grade A, 74/100". */
414
+ badge: string;
415
+ /** Accessible name of the review: its author, date and score. */
416
+ label: string;
417
+ /** Hidden by the community and not revealed: no text. */
418
+ masked: boolean;
419
+ /** The text shown, cut short while folded; `""` for a review without text. */
420
+ text: string;
421
+ /** Style classes of the text: folded to three lines or whole. */
422
+ textClass: string;
423
+ /** "Read more" or "Show less" shows, with this label; `""` for none. */
424
+ fold: string;
425
+ /** The "Translated" mark shows: a translation is the text shown. */
426
+ translated: boolean;
427
+ /** "Show original" or "Show translation"; `""` for none. */
428
+ language: string;
429
+ /** The row of small buttons shows. */
430
+ actions: boolean;
431
+ }
432
+
433
+ const NO_ROW: Row = {
434
+ id: "",
435
+ author: "",
436
+ authorTip: "",
437
+ date: "",
438
+ score: "",
439
+ badge: "",
440
+ label: "",
441
+ masked: false,
442
+ text: "",
443
+ textClass: "body",
444
+ fold: "",
445
+ translated: false,
446
+ language: "",
447
+ actions: false,
448
+ };
449
+
450
+ /** Whether the page shown has a review at `index`. */
451
+ export function has(page: ReviewsPage | null, index: number): boolean {
452
+ return page !== null && index < Math.min(page.items.length, PAGE_SIZE);
453
+ }
454
+
455
+ /**
456
+ * The review at `index` of the page shown. The buttons show in Interactive
457
+ * mode only; in Passive mode a translation keeps its "Translated" mark.
458
+ */
459
+ export function row(
460
+ page: ReviewsPage | null,
461
+ reading: Record<string, Reading>,
462
+ interactive: boolean,
463
+ index: number,
464
+ ): Row {
465
+ const review = has(page, index) ? page?.items[index] : undefined;
466
+ if (review === undefined) {
467
+ return NO_ROW;
468
+ }
469
+ const choice = reading[review.id] ?? UNREAD;
470
+ const author = oneLine(review.author);
471
+ const date = dateText(review);
472
+ const score = String(roundHalfAway(review.score));
473
+ const masked = review.hidden && !choice.revealed;
474
+ const text = masked ? "" : shownText(review, choice);
475
+ const long = folds(text);
476
+ const translated = !masked && text !== "" && isTranslated(review);
477
+ const fold = interactive && long ? t(choice.expanded ? "less" : "more") : "";
478
+ const language = interactive && translated ? t(choice.original ? "translation" : "original") : "";
479
+ return {
480
+ id: review.id,
481
+ author,
482
+ authorTip: cutBytes(author, MAX_LABEL_BYTES),
483
+ date,
484
+ score,
485
+ badge: badgeLabel(review.grade, review.score, { grade: t("grade"), none: t("no-grade") }),
486
+ label: cutBytes(t("review", { author, date, score }), MAX_LABEL_BYTES),
487
+ masked,
488
+ text: long && !choice.expanded ? foldedText(text) : text,
489
+ textClass: long && !choice.expanded ? "body folded" : "body",
490
+ fold,
491
+ translated: translated && !choice.original,
492
+ language,
493
+ actions: fold !== "" || language !== "",
494
+ };
495
+ }
496
+
497
+ /** "1 review", "1,284 reviews" in the user's number format. */
498
+ export function countText(page: ReviewsPage): string {
499
+ const count = Math.max(0, Math.floor(page.count));
500
+ return t(count === 1 ? "count-one" : "count-other", {
501
+ count: formatNumber(count, { maximumFractionDigits: 0 }),
502
+ });
503
+ }
504
+
505
+ /** "2 / 3". */
506
+ export function pagerText(page: ReviewsPage): string {
507
+ return t("pager", pageNumbers(page));
508
+ }
509
+
510
+ /** Accessible name of the pager: "Page 2 of 3". */
511
+ export function pagerLabel(page: ReviewsPage): string {
512
+ return t("page-of", pageNumbers(page));
513
+ }
514
+
515
+ function pageNumbers(page: ReviewsPage): { page: string; total: string } {
516
+ const total = Math.max(1, Math.floor(page.totalPages));
517
+ return {
518
+ page: String(Math.min(total, Math.max(1, Math.floor(page.page)))),
519
+ total: String(total),
520
+ };
521
+ }
522
+
523
+ /** The text shown without reviews: which list is empty. */
524
+ export function emptyText(followedOnly: boolean): string {
525
+ return t(followedOnly ? "empty-followed" : "empty");
526
+ }
527
+
528
+ /** The paging buttons act with PlayerVox reachable and no read in flight. */
529
+ export function canTurn(
530
+ page: ReviewsPage | null,
531
+ direction: "previous" | "next",
532
+ busy: boolean,
533
+ offline: boolean,
534
+ ): boolean {
535
+ if (page === null || busy || offline) {
536
+ return false;
537
+ }
538
+ return direction === "previous" ? page.page > 1 : page.page < page.totalPages;
539
+ }
540
+
541
+ /** The message key of a failed read. */
542
+ export function readProblem(code: string): string {
543
+ switch (code) {
544
+ case "not_connected":
545
+ return "read-expired";
546
+ case "busy":
547
+ return "read-busy";
548
+ default:
549
+ return "read-failed";
550
+ }
551
+ }
552
+
553
+ /** The line above the reviews: the last failed read; `""` for none. */
554
+ export function message(problem: string): string {
555
+ return problem === "" ? "" : t(problem);
556
+ }
557
+
558
+ /** Number of the page shown, or to show; the first page after a reset. */
559
+ let pageNumber = 1;
560
+ /** The reviews revision the page shown was read for. */
561
+ let revision: number | null = null;
562
+ /** The revision changed while nothing could be read (offline). */
563
+ let stale = false;
564
+ let reading = false;
565
+ /** The reviews changed during a read: read once more after it. */
566
+ let again = false;
567
+ let rereads = 0;
568
+ let reread: Timer | null = null;
569
+ /** Bumped when the page to show changes: a read answered after it is dropped. */
570
+ let epoch = 0;
571
+
572
+ /** The badges of the page shown, one canvas per review. */
573
+ function drawBadges(): void {
574
+ for (let index = 0; index < PAGE_SIZE; index += 1) {
575
+ const review = state.page?.items[index];
576
+ draw(
577
+ `badge-${index}`,
578
+ review === undefined ? [] : smallBadge(review.grade, host.theme),
579
+ );
580
+ }
581
+ }
582
+
583
+ /** Side of a review's badge canvas: the shared badge at 42 %. */
584
+ export const SMALL_BADGE = 32;
585
+
586
+ /** The shared badge scaled to a `SMALL_BADGE` canvas, centred. */
587
+ export function smallBadge(grade: BadgeGrade, theme: "dark" | "light"): DrawCommand[] {
588
+ const scale = SMALL_BADGE / BADGE_CANVAS;
589
+ return [["save"], ["scale", scale, scale], ...badgeCommands(grade, theme), ["restore"]];
590
+ }
591
+
592
+ /**
593
+ * Keeps the reading choices of the reviews still on the page; another
594
+ * page or filter starts unread.
595
+ */
596
+ function show(page: ReviewsPage, same: boolean): void {
597
+ const kept: Record<string, Reading> = {};
598
+ if (same) {
599
+ for (const review of page.items) {
600
+ const choice = state.reading[review.id];
601
+ if (choice !== undefined) {
602
+ kept[review.id] = choice;
603
+ }
604
+ }
605
+ }
606
+ state.reading = kept;
607
+ state.page = page;
608
+ state.phase = "ready";
609
+ drawBadges();
610
+ }
611
+
612
+ /** Reads page `number` with the widget's filter and shows it. */
613
+ function read(number: number): void {
614
+ if (reading) {
615
+ again = true;
616
+ return;
617
+ }
618
+ reread?.cancel();
619
+ reread = null;
620
+ reading = true;
621
+ state.busy = true;
622
+ const started = epoch;
623
+ const followedOnly = option("followed-only", false);
624
+ playervox.reviews.page({ page: number, followedOnly }).then(
625
+ (page) => {
626
+ // The game, the filter or the language changed meanwhile: this
627
+ // answer is no longer the widget's, and another read may be running.
628
+ if (started !== epoch) {
629
+ return;
630
+ }
631
+ reading = false;
632
+ rereads = 0;
633
+ const same =
634
+ state.page !== null && state.followedOnly === followedOnly && state.page.page === page.page;
635
+ // PlayerVox answers its last page for a page that no longer exists.
636
+ pageNumber = Math.max(1, Math.floor(page.page));
637
+ state.followedOnly = followedOnly;
638
+ state.problem = "";
639
+ show(page, same);
640
+ finish();
641
+ },
642
+ (error: ServiceError) => {
643
+ if (started !== epoch) {
644
+ return;
645
+ }
646
+ reading = false;
647
+ failed(error.code, number);
648
+ finish();
649
+ },
650
+ );
651
+ }
652
+
653
+ function finish(): void {
654
+ state.busy = reading;
655
+ if (again) {
656
+ again = false;
657
+ read(pageNumber);
658
+ }
659
+ }
660
+
661
+ function failed(code: string, number: number): void {
662
+ if ((code === "busy" || code === "stale_context") && rereads < MAX_REREADS) {
663
+ // The host was busy, or the game or account changed under the read:
664
+ // ask again shortly, from the first page after a change.
665
+ rereads += 1;
666
+ const next = code === "busy" ? number : 1;
667
+ reread = timers.after(REREAD_MS, () => {
668
+ reread = null;
669
+ read(next);
670
+ });
671
+ return;
672
+ }
673
+ // The page shown stays; the next change of the reviews reads again.
674
+ state.problem = readProblem(code);
675
+ if (state.page === null) {
676
+ state.phase = "failed";
677
+ }
678
+ }
679
+
680
+ /** Shows the previous or next page; one request at a time. */
681
+ export function turn(direction: "previous" | "next"): void {
682
+ if (!state.interactive || !canTurn(state.page, direction, state.busy, state.offline)) {
683
+ return;
684
+ }
685
+ state.problem = "";
686
+ rereads = 0;
687
+ read((state.page?.page ?? 1) + (direction === "next" ? 1 : -1));
688
+ }
689
+
690
+ function choose(id: string, change: Partial<Reading>): void {
691
+ if (!state.interactive || id === "") {
692
+ return;
693
+ }
694
+ state.reading = { ...state.reading, [id]: { ...(state.reading[id] ?? UNREAD), ...change } };
695
+ }
696
+
697
+ /** Unfolds or folds a long review. */
698
+ export function toggleFold(id: string): void {
699
+ choose(id, { expanded: !(state.reading[id]?.expanded ?? false) });
700
+ }
701
+
702
+ /** Shows the untranslated text of a translated review, or its translation. */
703
+ export function toggleLanguage(id: string): void {
704
+ choose(id, { original: !(state.reading[id]?.original ?? false) });
705
+ }
706
+
707
+ /** Shows a review the community hid. */
708
+ export function reveal(id: string): void {
709
+ choose(id, { revealed: true });
710
+ }
711
+
712
+ /** Nothing of the last game, account or filter stays. */
713
+ function forget(): void {
714
+ reread?.cancel();
715
+ reread = null;
716
+ epoch += 1;
717
+ reading = false;
718
+ again = false;
719
+ rereads = 0;
720
+ stale = false;
721
+ pageNumber = 1;
722
+ state.page = null;
723
+ state.reading = {};
724
+ state.busy = false;
725
+ drawBadges();
726
+ }
727
+
728
+ function clear(phase: Phase, problem: string): void {
729
+ forget();
730
+ revision = null;
731
+ state.problem = problem;
732
+ state.phase = phase;
733
+ }
734
+
735
+ /**
736
+ * The first page again, for another filter or language. Without reviews to
737
+ * read (no game, no Steam app ID, no source, nothing published yet), there
738
+ * is nothing to read again.
739
+ */
740
+ function restart(): void {
741
+ if (revision === null) {
742
+ return;
743
+ }
744
+ forget();
745
+ state.problem = "";
746
+ state.phase = "loading";
747
+ if (state.offline) {
748
+ stale = true;
749
+ } else {
750
+ read(1);
751
+ }
752
+ }
753
+
754
+ let subscription: Subscription | null = null;
755
+
756
+ function subscribe(): void {
757
+ const current = playervox.reviews.subscribe((update) => {
758
+ if (!update.ok) {
759
+ // The subscription ended: the host's reviews source failed. Show it
760
+ // and ask again later; hidden, the timer waits until the widget shows.
761
+ if (subscription === current) {
762
+ subscription = null;
763
+ clear("failed", "source-failed");
764
+ timers.after(RETRY_MS, subscribe);
765
+ }
766
+ return;
767
+ }
768
+ const value = update.value;
769
+ state.offline = value.offline;
770
+ if (value.state !== "ready") {
771
+ clear(value.state === "idle" ? "idle" : "unsupported", "");
772
+ return;
773
+ }
774
+ if (state.phase !== "ready" && state.phase !== "loading") {
775
+ state.phase = "loading";
776
+ state.problem = "";
777
+ }
778
+ // A new revision, or a page still missing after a failure: read the
779
+ // page shown again. Offline nothing is read: the page shown stays
780
+ // until PlayerVox is back.
781
+ if (value.revision !== revision || state.page === null) {
782
+ revision = value.revision;
783
+ stale = true;
784
+ }
785
+ if (stale && !value.offline) {
786
+ stale = false;
787
+ rereads = 0;
788
+ read(pageNumber);
789
+ }
790
+ });
791
+ subscription = current;
792
+ }
793
+
794
+ onHost((changed) => {
795
+ if (changed.includes("mode")) {
796
+ state.interactive = host.mode === "interactive";
797
+ }
798
+ if (changed.includes("theme")) {
799
+ // The neutral badge follows the theme; the mark's token does already.
800
+ drawBadges();
801
+ }
802
+ // Another filter, or another language (the host reads the reviews in the
803
+ // language of its interface): the first page, unread.
804
+ if (
805
+ (changed.includes("options") && option("followed-only", false) !== filter) ||
806
+ (changed.includes("locale") && host.locale !== locale)
807
+ ) {
808
+ filter = option("followed-only", false);
809
+ locale = host.locale;
810
+ restart();
811
+ }
812
+ });
813
+
814
+ let filter = option("followed-only", false);
815
+ let locale = host.locale;
816
+
817
+ draw("mark", markCommands(16, 18, "var(--color-text-muted)"));
818
+ // Without the grant the host would refuse the subscription: say so
819
+ // instead of asking.
820
+ if (canRead) {
821
+ subscribe();
822
+ }