@skillcdn/console 0.4.0 → 0.4.2

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 (308) hide show
  1. package/README.md +20 -2
  2. package/SKILLCDN.md +7 -5
  3. package/dist/api.d.ts +5 -4
  4. package/dist/api.d.ts.map +1 -1
  5. package/dist/api.js +5 -4
  6. package/dist/api.js.map +1 -1
  7. package/dist/cli/cli.d.ts.map +1 -1
  8. package/dist/cli/cli.js +579 -45
  9. package/dist/cli/cli.js.map +1 -1
  10. package/dist/cli/format.d.ts +34 -3
  11. package/dist/cli/format.d.ts.map +1 -1
  12. package/dist/cli/format.js +277 -6
  13. package/dist/cli/format.js.map +1 -1
  14. package/dist/cli/help.d.ts.map +1 -1
  15. package/dist/cli/help.js +60 -7
  16. package/dist/cli/help.js.map +1 -1
  17. package/dist/cli/plugin-check.d.ts +21 -0
  18. package/dist/cli/plugin-check.d.ts.map +1 -0
  19. package/dist/cli/plugin-check.js +374 -0
  20. package/dist/cli/plugin-check.js.map +1 -0
  21. package/dist/cli/plugin-cli.d.ts +20 -0
  22. package/dist/cli/plugin-cli.d.ts.map +1 -0
  23. package/dist/cli/plugin-cli.js +123 -0
  24. package/dist/cli/plugin-cli.js.map +1 -0
  25. package/dist/cli/plugin-init.d.ts +20 -0
  26. package/dist/cli/plugin-init.d.ts.map +1 -0
  27. package/dist/cli/plugin-init.js +74 -0
  28. package/dist/cli/plugin-init.js.map +1 -0
  29. package/dist/cli/plugin-main.d.ts +3 -0
  30. package/dist/cli/plugin-main.d.ts.map +1 -0
  31. package/dist/cli/plugin-main.js +28 -0
  32. package/dist/cli/plugin-main.js.map +1 -0
  33. package/dist/client.d.ts +109 -2
  34. package/dist/client.d.ts.map +1 -1
  35. package/dist/client.js +67 -2
  36. package/dist/client.js.map +1 -1
  37. package/dist/compat.d.ts +1 -1
  38. package/dist/compat.js +2 -2
  39. package/dist/components/board.d.ts +8 -0
  40. package/dist/components/board.d.ts.map +1 -1
  41. package/dist/components/board.js +59 -15
  42. package/dist/components/board.js.map +1 -1
  43. package/dist/components/event-feed.d.ts +1 -1
  44. package/dist/components/event-feed.d.ts.map +1 -1
  45. package/dist/components/event-feed.js +33 -1
  46. package/dist/components/event-feed.js.map +1 -1
  47. package/dist/components/markdown.d.ts.map +1 -1
  48. package/dist/components/markdown.js +6 -3
  49. package/dist/components/markdown.js.map +1 -1
  50. package/dist/components/pack-connect.d.ts +3 -1
  51. package/dist/components/pack-connect.d.ts.map +1 -1
  52. package/dist/components/pack-connect.js +4 -4
  53. package/dist/components/pack-connect.js.map +1 -1
  54. package/dist/components/pack-token.d.ts +3 -1
  55. package/dist/components/pack-token.d.ts.map +1 -1
  56. package/dist/components/pack-token.js +1 -1
  57. package/dist/components/pack-token.js.map +1 -1
  58. package/dist/components/people.d.ts.map +1 -1
  59. package/dist/components/people.js +14 -3
  60. package/dist/components/people.js.map +1 -1
  61. package/dist/components/plugin-frame.d.ts +52 -0
  62. package/dist/components/plugin-frame.d.ts.map +1 -0
  63. package/dist/components/plugin-frame.js +314 -0
  64. package/dist/components/plugin-frame.js.map +1 -0
  65. package/dist/components/plugin-settings.d.ts +38 -0
  66. package/dist/components/plugin-settings.d.ts.map +1 -0
  67. package/dist/components/plugin-settings.js +348 -0
  68. package/dist/components/plugin-settings.js.map +1 -0
  69. package/dist/components/remote-pictures.d.ts +6 -0
  70. package/dist/components/remote-pictures.d.ts.map +1 -0
  71. package/dist/components/remote-pictures.js +8 -0
  72. package/dist/components/remote-pictures.js.map +1 -0
  73. package/dist/components/shell.d.ts +2 -0
  74. package/dist/components/shell.d.ts.map +1 -1
  75. package/dist/components/shell.js +26 -1
  76. package/dist/components/shell.js.map +1 -1
  77. package/dist/components/sign-in.d.ts.map +1 -1
  78. package/dist/components/sign-in.js +49 -2
  79. package/dist/components/sign-in.js.map +1 -1
  80. package/dist/components/ui.d.ts +3 -2
  81. package/dist/components/ui.d.ts.map +1 -1
  82. package/dist/components/ui.js +6 -3
  83. package/dist/components/ui.js.map +1 -1
  84. package/dist/components/workspace.d.ts +7 -1
  85. package/dist/components/workspace.d.ts.map +1 -1
  86. package/dist/components/workspace.js +1 -1
  87. package/dist/components/workspace.js.map +1 -1
  88. package/dist/console.css +384 -5
  89. package/dist/console.d.ts.map +1 -1
  90. package/dist/console.js +146 -29
  91. package/dist/console.js.map +1 -1
  92. package/dist/data.d.ts +11 -2
  93. package/dist/data.d.ts.map +1 -1
  94. package/dist/data.js +35 -21
  95. package/dist/data.js.map +1 -1
  96. package/dist/i18n/en.d.ts +163 -1
  97. package/dist/i18n/en.d.ts.map +1 -1
  98. package/dist/i18n/en.js +174 -1
  99. package/dist/i18n/en.js.map +1 -1
  100. package/dist/i18n/ko.d.ts.map +1 -1
  101. package/dist/i18n/ko.js +154 -1
  102. package/dist/i18n/ko.js.map +1 -1
  103. package/dist/index.d.ts +3 -1
  104. package/dist/index.d.ts.map +1 -1
  105. package/dist/index.js +3 -1
  106. package/dist/index.js.map +1 -1
  107. package/dist/limits.d.ts +14 -0
  108. package/dist/limits.d.ts.map +1 -1
  109. package/dist/limits.js +14 -0
  110. package/dist/limits.js.map +1 -1
  111. package/dist/plugin/args.d.ts +148 -0
  112. package/dist/plugin/args.d.ts.map +1 -0
  113. package/dist/plugin/args.js +369 -0
  114. package/dist/plugin/args.js.map +1 -0
  115. package/dist/plugin/bridge.d.ts +146 -0
  116. package/dist/plugin/bridge.d.ts.map +1 -0
  117. package/dist/plugin/bridge.js +125 -0
  118. package/dist/plugin/bridge.js.map +1 -0
  119. package/dist/plugin/capabilities.d.ts +20 -0
  120. package/dist/plugin/capabilities.d.ts.map +1 -0
  121. package/dist/plugin/capabilities.js +133 -0
  122. package/dist/plugin/capabilities.js.map +1 -0
  123. package/dist/plugin/context.d.ts +374 -0
  124. package/dist/plugin/context.d.ts.map +1 -0
  125. package/dist/plugin/context.js +2 -0
  126. package/dist/plugin/context.js.map +1 -0
  127. package/dist/plugin/define.d.ts +208 -0
  128. package/dist/plugin/define.d.ts.map +1 -0
  129. package/dist/plugin/define.js +482 -0
  130. package/dist/plugin/define.js.map +1 -0
  131. package/dist/plugin/definition-error.d.ts +11 -0
  132. package/dist/plugin/definition-error.d.ts.map +1 -0
  133. package/dist/plugin/definition-error.js +17 -0
  134. package/dist/plugin/definition-error.js.map +1 -0
  135. package/dist/plugin/limits.d.ts +67 -0
  136. package/dist/plugin/limits.d.ts.map +1 -0
  137. package/dist/plugin/limits.js +70 -0
  138. package/dist/plugin/limits.js.map +1 -0
  139. package/dist/plugin/load-worker.d.ts +7 -0
  140. package/dist/plugin/load-worker.d.ts.map +1 -0
  141. package/dist/plugin/load-worker.js +41 -0
  142. package/dist/plugin/load-worker.js.map +1 -0
  143. package/dist/plugin/load.d.ts +18 -0
  144. package/dist/plugin/load.d.ts.map +1 -0
  145. package/dist/plugin/load.js +62 -0
  146. package/dist/plugin/load.js.map +1 -0
  147. package/dist/plugin/manifest.d.ts +237 -0
  148. package/dist/plugin/manifest.d.ts.map +1 -0
  149. package/dist/plugin/manifest.js +411 -0
  150. package/dist/plugin/manifest.js.map +1 -0
  151. package/dist/plugin/refusals.d.ts +49 -0
  152. package/dist/plugin/refusals.d.ts.map +1 -0
  153. package/dist/plugin/refusals.js +79 -0
  154. package/dist/plugin/refusals.js.map +1 -0
  155. package/dist/plugin/rest-schemas.d.ts +556 -0
  156. package/dist/plugin/rest-schemas.d.ts.map +1 -0
  157. package/dist/plugin/rest-schemas.js +262 -0
  158. package/dist/plugin/rest-schemas.js.map +1 -0
  159. package/dist/plugin/sandbox.d.ts +23 -0
  160. package/dist/plugin/sandbox.d.ts.map +1 -0
  161. package/dist/plugin/sandbox.js +99 -0
  162. package/dist/plugin/sandbox.js.map +1 -0
  163. package/dist/plugin/testing.d.ts +45 -0
  164. package/dist/plugin/testing.d.ts.map +1 -0
  165. package/dist/plugin/testing.js +420 -0
  166. package/dist/plugin/testing.js.map +1 -0
  167. package/dist/plugin/web-react.d.ts +11 -0
  168. package/dist/plugin/web-react.d.ts.map +1 -0
  169. package/dist/plugin/web-react.js +34 -0
  170. package/dist/plugin/web-react.js.map +1 -0
  171. package/dist/plugin/web.d.ts +95 -0
  172. package/dist/plugin/web.d.ts.map +1 -0
  173. package/dist/plugin/web.js +245 -0
  174. package/dist/plugin/web.js.map +1 -0
  175. package/dist/plugin-nav.d.ts +29 -0
  176. package/dist/plugin-nav.d.ts.map +1 -0
  177. package/dist/plugin-nav.js +92 -0
  178. package/dist/plugin-nav.js.map +1 -0
  179. package/dist/plugin-testing.d.ts +2 -0
  180. package/dist/plugin-testing.d.ts.map +1 -0
  181. package/dist/plugin-testing.js +4 -0
  182. package/dist/plugin-testing.js.map +1 -0
  183. package/dist/plugin-web-react.d.ts +3 -0
  184. package/dist/plugin-web-react.d.ts.map +1 -0
  185. package/dist/plugin-web-react.js +3 -0
  186. package/dist/plugin-web-react.js.map +1 -0
  187. package/dist/plugin-web.d.ts +4 -0
  188. package/dist/plugin-web.d.ts.map +1 -0
  189. package/dist/plugin-web.js +5 -0
  190. package/dist/plugin-web.js.map +1 -0
  191. package/dist/plugin.d.ts +11 -0
  192. package/dist/plugin.d.ts.map +1 -0
  193. package/dist/plugin.js +15 -0
  194. package/dist/plugin.js.map +1 -0
  195. package/dist/router.d.ts +28 -0
  196. package/dist/router.d.ts.map +1 -1
  197. package/dist/router.js +47 -0
  198. package/dist/router.js.map +1 -1
  199. package/dist/routes.d.ts +6 -0
  200. package/dist/routes.d.ts.map +1 -1
  201. package/dist/routes.js +8 -0
  202. package/dist/routes.js.map +1 -1
  203. package/dist/schemas.d.ts +147 -1
  204. package/dist/schemas.d.ts.map +1 -1
  205. package/dist/schemas.js +55 -5
  206. package/dist/schemas.js.map +1 -1
  207. package/dist/tokens.d.ts +2 -0
  208. package/dist/tokens.d.ts.map +1 -0
  209. package/dist/tokens.js +101 -0
  210. package/dist/tokens.js.map +1 -0
  211. package/dist/vocabulary.d.ts +11 -1
  212. package/dist/vocabulary.d.ts.map +1 -1
  213. package/dist/vocabulary.js +24 -0
  214. package/dist/vocabulary.js.map +1 -1
  215. package/dist/web.d.ts.map +1 -1
  216. package/dist/web.js +5 -2
  217. package/dist/web.js.map +1 -1
  218. package/docs/specs/cli.md +16 -9
  219. package/docs/specs/plugins.md +487 -0
  220. package/docs/specs/rest.md +25 -13
  221. package/package.json +21 -3
  222. package/skills/building-a-console/SKILL.md +25 -26
  223. package/skills/building-a-console/references/configuring.md +40 -0
  224. package/skills/building-a-console/references/the-default-console.md +1 -1
  225. package/skills/building-a-console/references/the-look-from-a-website.md +5 -5
  226. package/skills/building-a-plugin/SKILL.md +137 -0
  227. package/skills/deploying-a-console/SKILL.md +24 -26
  228. package/skills/deploying-a-console/references/access.md +13 -9
  229. package/skills/deploying-a-console/references/resources.md +25 -10
  230. package/skills/deploying-a-console/references/running-it-again.md +15 -0
  231. package/skills/deploying-a-console/references/sizing.md +2 -0
  232. package/skills/planning-the-board/SKILL.md +96 -0
  233. package/skills/planning-the-board/references/a-task.md +36 -0
  234. package/skills/planning-the-board/references/setting-up.md +44 -0
  235. package/skills/working-the-board/SKILL.md +43 -34
  236. package/skills/working-the-board/references/the-pack.md +19 -0
  237. package/src/api.ts +16 -0
  238. package/src/cli/cli.ts +736 -45
  239. package/src/cli/format.ts +356 -9
  240. package/src/cli/help.ts +71 -6
  241. package/src/cli/plugin-check.ts +470 -0
  242. package/src/cli/plugin-cli.ts +148 -0
  243. package/src/cli/plugin-init.ts +90 -0
  244. package/src/cli/plugin-main.ts +30 -0
  245. package/src/client.ts +324 -1
  246. package/src/compat.ts +2 -2
  247. package/src/components/board.tsx +62 -14
  248. package/src/components/event-feed.tsx +42 -1
  249. package/src/components/markdown.tsx +6 -3
  250. package/src/components/pack-connect.tsx +10 -7
  251. package/src/components/pack-token.tsx +4 -1
  252. package/src/components/people.tsx +17 -7
  253. package/src/components/plugin-frame.tsx +437 -0
  254. package/src/components/plugin-settings.tsx +1111 -0
  255. package/src/components/remote-pictures.ts +19 -0
  256. package/src/components/shell.tsx +82 -14
  257. package/src/components/sign-in.tsx +114 -13
  258. package/src/components/ui.tsx +6 -3
  259. package/src/components/workspace.tsx +18 -0
  260. package/src/console.tsx +309 -23
  261. package/src/data.ts +47 -22
  262. package/src/i18n/en.ts +183 -1
  263. package/src/i18n/ko.ts +159 -1
  264. package/src/index.ts +12 -0
  265. package/src/limits.ts +14 -0
  266. package/src/plugin/args.ts +526 -0
  267. package/src/plugin/bridge.ts +314 -0
  268. package/src/plugin/capabilities.ts +150 -0
  269. package/src/plugin/context.ts +416 -0
  270. package/src/plugin/define.ts +792 -0
  271. package/src/plugin/definition-error.ts +19 -0
  272. package/src/plugin/limits.ts +70 -0
  273. package/src/plugin/load-worker.ts +50 -0
  274. package/src/plugin/load.ts +82 -0
  275. package/src/plugin/manifest.ts +561 -0
  276. package/src/plugin/refusals.ts +94 -0
  277. package/src/plugin/rest-schemas.ts +354 -0
  278. package/src/plugin/sandbox.ts +123 -0
  279. package/src/plugin/testing.ts +612 -0
  280. package/src/plugin/web-react.ts +45 -0
  281. package/src/plugin/web.ts +403 -0
  282. package/src/plugin-nav.ts +132 -0
  283. package/src/plugin-testing.ts +10 -0
  284. package/src/plugin-web-react.ts +4 -0
  285. package/src/plugin-web.ts +35 -0
  286. package/src/plugin.ts +126 -0
  287. package/src/router.ts +73 -0
  288. package/src/routes.ts +9 -0
  289. package/src/schemas.ts +60 -4
  290. package/src/styles/console.css +384 -5
  291. package/src/tokens.ts +101 -0
  292. package/src/vocabulary.ts +30 -0
  293. package/src/web.ts +5 -2
  294. package/template/plugin/.gitignore +2 -0
  295. package/template/plugin/README.md +26 -0
  296. package/template/plugin/SKILLCDN.md +13 -0
  297. package/template/plugin/build.mjs +40 -0
  298. package/template/plugin/console-plugin.json +50 -0
  299. package/template/plugin/package.json +27 -0
  300. package/template/plugin/skills/__key__/SKILL.md +23 -0
  301. package/template/plugin/src/check.test.ts +34 -0
  302. package/template/plugin/src/server/index.test.ts +41 -0
  303. package/template/plugin/src/server/index.ts +72 -0
  304. package/template/plugin/src/web/index.css +51 -0
  305. package/template/plugin/src/web/index.ts +115 -0
  306. package/template/plugin/tsconfig.json +17 -0
  307. package/skills/working-the-board/references/setting-up.md +0 -3
  308. /package/skills/{working-the-board → planning-the-board}/references/project-rules.md +0 -0
@@ -0,0 +1,90 @@
1
+ // `console-plugin init` (docs/specs/plugins.md, "Authoring"): writes a plugin from the template
2
+ // the package ships, the words `__key__`, `__name__` and `__console__` in the template's files and
3
+ // their names replaced. `examples/plugin-hello` in the console's repository is what it writes for
4
+ // the key `hello`, and a test holds the two equal.
5
+ import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
6
+ import { join, relative, sep } from "node:path";
7
+
8
+ export interface InitOptions {
9
+ readonly key: string;
10
+ /** The plugin's name for people, in English. */
11
+ readonly name: string;
12
+ /** What the plugin's devDependency on `@skillcdn/console` says: `^<version>` of the command, unless said. */
13
+ readonly consoleDependency: string;
14
+ }
15
+
16
+ /** The words the template uses, and what each becomes. */
17
+ export function substitutions(options: InitOptions): readonly [string, string][] {
18
+ return [
19
+ ["__key__", options.key],
20
+ ["__name__", options.name],
21
+ ["__console__", options.consoleDependency],
22
+ ];
23
+ }
24
+
25
+ export function substitute(text: string, options: InitOptions): string {
26
+ let result = text;
27
+ for (const [word, value] of substitutions(options)) {
28
+ result = result.replaceAll(word, value);
29
+ }
30
+ return result;
31
+ }
32
+
33
+ /** A name for people from a key: `leave-requests` is "Leave Requests". */
34
+ export function nameFromKey(key: string): string {
35
+ return key
36
+ .split("-")
37
+ .filter((word) => word.length > 0)
38
+ .map((word) => `${word[0]?.toUpperCase() ?? ""}${word.slice(1)}`)
39
+ .join(" ");
40
+ }
41
+
42
+ async function* walk(dir: string): AsyncGenerator<string> {
43
+ for (const entry of await readdir(dir, { withFileTypes: true })) {
44
+ const path = join(dir, entry.name);
45
+ if (entry.isDirectory()) {
46
+ yield* walk(path);
47
+ } else if (entry.isFile()) {
48
+ yield path;
49
+ }
50
+ }
51
+ }
52
+
53
+ /** The files of the template, as paths relative to it with forward slashes. */
54
+ export async function templateFiles(templateDir: string): Promise<string[]> {
55
+ const files: string[] = [];
56
+ for await (const path of walk(templateDir)) {
57
+ files.push(relative(templateDir, path).split(sep).join("/"));
58
+ }
59
+ return files.sort();
60
+ }
61
+
62
+ /**
63
+ * Writes the template into `targetDir`, which must not exist or be an empty directory, and
64
+ * answers the files written, relative to it.
65
+ */
66
+ export async function writePluginFromTemplate(
67
+ templateDir: string,
68
+ targetDir: string,
69
+ options: InitOptions,
70
+ ): Promise<string[]> {
71
+ const found = await stat(targetDir).catch(() => null);
72
+ if (found !== null) {
73
+ if (!found.isDirectory()) {
74
+ throw new Error(`${targetDir} exists and is not a directory`);
75
+ }
76
+ if ((await readdir(targetDir)).length > 0) {
77
+ throw new Error(`${targetDir} is not empty`);
78
+ }
79
+ }
80
+ const written: string[] = [];
81
+ for (const file of await templateFiles(templateDir)) {
82
+ const target = substitute(file, options);
83
+ const text = await readFile(join(templateDir, file), "utf8");
84
+ const path = join(targetDir, ...target.split("/"));
85
+ await mkdir(join(path, ".."), { recursive: true });
86
+ await writeFile(path, substitute(text, options), "utf8");
87
+ written.push(target);
88
+ }
89
+ return written;
90
+ }
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
3
+ import { join } from "node:path";
4
+ import process from "node:process";
5
+ import { fileURLToPath } from "node:url";
6
+ import * as z from "zod/mini";
7
+ import { loadServerModule } from "../plugin/load.js";
8
+ import { runPluginCli } from "./plugin-cli.js";
9
+
10
+ // The `console-plugin` command, the package's second bin: the only place it reads the process.
11
+ // Everything it does is in plugin-cli.ts, which takes what it needs and is what the tests run.
12
+
13
+ /** The package's own directory, two levels above dist/cli: where the template is. */
14
+ const packageDir = fileURLToPath(new URL("../../", import.meta.url));
15
+ const manifest = z
16
+ .object({ version: z.string() })
17
+ .safeParse(createRequire(import.meta.url)("../../package.json"));
18
+
19
+ process.exitCode = await runPluginCli(process.argv.slice(2), {
20
+ stdout: (text) => {
21
+ process.stdout.write(text);
22
+ },
23
+ stderr: (text) => {
24
+ process.stderr.write(text);
25
+ },
26
+ cwd: process.cwd(),
27
+ templateDir: join(packageDir, "template", "plugin"),
28
+ version: manifest.success ? manifest.data.version : undefined,
29
+ loadServer: (file) => loadServerModule(file),
30
+ });
package/src/client.ts CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  connectPath,
6
6
  personTokensPath,
7
7
  personWorkPath,
8
+ pluginPath,
8
9
  projectPath,
9
10
  REST_ROUTES,
10
11
  restPath,
@@ -52,6 +53,21 @@ import {
52
53
  type RestPerson,
53
54
  type RestPersonPatch,
54
55
  type RestPersonWork,
56
+ type RestPluginCommandInput,
57
+ type RestPluginDetail,
58
+ type RestPluginEnableInput,
59
+ type RestPluginEvents,
60
+ type RestPluginFileHandle,
61
+ type RestPluginGrant,
62
+ type RestPluginGrantInput,
63
+ type RestPluginGrants,
64
+ type RestPluginInvocation,
65
+ type RestPluginInvokeInput,
66
+ type RestPluginRecordPage,
67
+ type RestPluginRecordRead,
68
+ type RestPluginRecordVersions,
69
+ type RestPluginSettingsPatch,
70
+ type RestPlugins,
55
71
  type RestProject,
56
72
  type RestProjectInput,
57
73
  type RestProjectPatch,
@@ -108,6 +124,16 @@ import {
108
124
  restPeopleSchema,
109
125
  restPersonSchema,
110
126
  restPersonWorkSchema,
127
+ restPluginDetailSchema,
128
+ restPluginEventsSchema,
129
+ restPluginFileHandleSchema,
130
+ restPluginGrantSchema,
131
+ restPluginGrantsSchema,
132
+ restPluginInvocationSchema,
133
+ restPluginRecordPageSchema,
134
+ restPluginRecordReadSchema,
135
+ restPluginRecordVersionsSchema,
136
+ restPluginsSchema,
111
137
  restProjectRulesSchema,
112
138
  restProjectSchema,
113
139
  restProjectsSchema,
@@ -208,6 +234,8 @@ export interface EventFilter {
208
234
  readonly run?: string | undefined;
209
235
  readonly decision?: string | undefined;
210
236
  readonly document?: string | undefined;
237
+ /** True for the newest events, oldest first, and whether there are earlier ones; `after` is not looked at then. */
238
+ readonly latest?: boolean | undefined;
211
239
  }
212
240
 
213
241
  /** One page of a listing: how many items, from which one on; the server's bounds apply. */
@@ -364,8 +392,14 @@ export interface ProjectClient {
364
392
  removePackToken(): Promise<RestProject>;
365
393
  /** Asks the deployment to connect the project's pack with a code a person approves (ADR-0036); for an owner. */
366
394
  connectPackToken(): Promise<RestPackConnectStarted>;
367
- /** How that connection stands; the token is kept when the person approved. For an owner. */
395
+ /** How that connection stands, read and changed nothing: pending, ended, or none; for an owner. */
368
396
  packTokenConnection(signal?: AbortSignal): Promise<RestPackConnect>;
397
+ /**
398
+ * Asks the console to claim the token at the deployment (ADR-0046): pending while the person
399
+ * has not approved, connected once, the token kept and the project as it stands, or ended;
400
+ * for an owner.
401
+ */
402
+ claimPackTokenConnection(signal?: AbortSignal): Promise<RestPackConnect>;
369
403
  /** Gives the connection up; for an owner. */
370
404
  cancelPackTokenConnection(): Promise<void>;
371
405
  /** The rules that hold in the project (ADR-0023): the workspace's, and what the project adds. */
@@ -396,6 +430,126 @@ export interface ProjectClient {
396
430
  updateGoal(id: string, patch: RestGoalPatch): Promise<RestGoal>;
397
431
  }
398
432
 
433
+ /** What a read of a plugin's collection is narrowed by (docs/specs/plugins.md, "Records"). */
434
+ export interface RecordFilter {
435
+ /** Conditions on indexed fields, as `ctx.records().list` takes them. */
436
+ readonly where?: Readonly<Record<string, unknown>> | undefined;
437
+ /** A field, with `-` before it for descending. */
438
+ readonly order?: string | undefined;
439
+ readonly limit?: number | undefined;
440
+ /** The cursor the last page answered with. */
441
+ readonly after?: string | undefined;
442
+ /** Words to search the collection's search fields for. */
443
+ readonly q?: string | undefined;
444
+ /** The key of a project, for the records linked to it. */
445
+ readonly project?: string | undefined;
446
+ readonly archived?: boolean | undefined;
447
+ }
448
+
449
+ /**
450
+ * The client of one plugin (docs/specs/plugins.md, "REST"): its commands and queries run as the
451
+ * person the client acts as, its records read within their roles, its skills, its events; and,
452
+ * for an administrator, enabling, configuring and granting. Every answer is in the console's
453
+ * generic shapes; what a command or a query itself answers is the plugin's own.
454
+ */
455
+ export interface PluginClient {
456
+ readonly key: string;
457
+ /** The plugin whole: its commands and queries with their arguments, its skills, its settings. */
458
+ get(signal?: AbortSignal): Promise<RestPluginDetail>;
459
+ /** Runs a command or a query by the words as typed, which the console parses against the definition. */
460
+ invoke(input: RestPluginInvokeInput): Promise<RestPluginInvocation>;
461
+ /** Runs a command with its arguments as JSON, by name, for a page. */
462
+ command(name: string, input: RestPluginCommandInput): Promise<RestPluginInvocation>;
463
+ /** Runs a query with its arguments as JSON; the answer is the query's own JSON. */
464
+ query(
465
+ name: string,
466
+ args: Readonly<Record<string, unknown>>,
467
+ options?: { readonly project?: string | undefined },
468
+ signal?: AbortSignal,
469
+ ): Promise<unknown>;
470
+ /** Where a query's answer is read as bytes, for one that answers a file. */
471
+ queryUrl(
472
+ name: string,
473
+ args: Readonly<Record<string, unknown>>,
474
+ options?: { readonly project?: string | undefined },
475
+ ): string;
476
+ /** Uploads a file for a command, with the `files` capability: the handle a `file` argument takes, good for an hour. */
477
+ upload(input: FileUpload): Promise<RestPluginFileHandle>;
478
+ /** A page of a collection's records, as the person may read them. */
479
+ records(
480
+ collection: string,
481
+ filter?: RecordFilter,
482
+ signal?: AbortSignal,
483
+ ): Promise<RestPluginRecordPage>;
484
+ /** One record, or one version of it. */
485
+ record(
486
+ collection: string,
487
+ id: string,
488
+ version?: number,
489
+ signal?: AbortSignal,
490
+ ): Promise<RestPluginRecordRead>;
491
+ recordVersions(
492
+ collection: string,
493
+ id: string,
494
+ signal?: AbortSignal,
495
+ ): Promise<RestPluginRecordVersions>;
496
+ /** Where a record's file is read, by its id. */
497
+ fileUrl(fileId: string): string;
498
+ /** The bytes of a record's file, with the name and the type the console says. */
499
+ readFile(fileId: string, signal?: AbortSignal): Promise<FileRead>;
500
+ /** Where the frame of a page or a widget is (ADR-0054), on a project when its key is given. */
501
+ frameUrl(options: {
502
+ readonly page?: string | undefined;
503
+ readonly widget?: string | undefined;
504
+ readonly project?: string | undefined;
505
+ }): string;
506
+ /** The events the plugin emitted after number `after`, oldest first: what a frame hears. */
507
+ pluginEvents(
508
+ page: { readonly after: number; readonly limit: number },
509
+ signal?: AbortSignal,
510
+ ): Promise<RestPluginEvents>;
511
+ /** Removes everything the console keeps of a disabled plugin, the key named again; for an administrator. */
512
+ purge(confirm: string): Promise<void>;
513
+ /** The plugin's skills, a pack of its own read from its package, as a project's five reads. */
514
+ skills(signal?: AbortSignal): Promise<RestSkills>;
515
+ skill(path: string, cursor?: string, signal?: AbortSignal): Promise<RestSkillLoad>;
516
+ skillFile(
517
+ path: string,
518
+ page?: { readonly offset?: number | undefined; readonly limit?: number | undefined },
519
+ signal?: AbortSignal,
520
+ ): Promise<RestSkillFile>;
521
+ skillBrowse(
522
+ page?: {
523
+ readonly path?: string | undefined;
524
+ readonly cursor?: string | undefined;
525
+ readonly limit?: number | undefined;
526
+ },
527
+ signal?: AbortSignal,
528
+ ): Promise<RestSkillBrowse>;
529
+ skillFind(
530
+ query: string,
531
+ options?: {
532
+ readonly path?: string | undefined;
533
+ readonly cursor?: string | undefined;
534
+ readonly limit?: number | undefined;
535
+ },
536
+ signal?: AbortSignal,
537
+ ): Promise<RestSkillFind>;
538
+ /** The plugin's events, newest first, a page at a time. */
539
+ events(page?: Page, signal?: AbortSignal): Promise<RestEvents>;
540
+ /** Enables the plugin, with consent to every capability it asks; for an administrator. */
541
+ enable(input: RestPluginEnableInput): Promise<RestPluginDetail>;
542
+ disable(): Promise<RestPluginDetail>;
543
+ /** Changes the settings' values, without secrets; `null` clears one. */
544
+ updateSettings(patch: RestPluginSettingsPatch): Promise<RestPluginDetail>;
545
+ /** Seals a secret for the plugin; it is answered by nothing from then on. */
546
+ setSecret(name: string, value: string): Promise<void>;
547
+ removeSecret(name: string): Promise<void>;
548
+ grants(signal?: AbortSignal): Promise<RestPluginGrants>;
549
+ grant(input: RestPluginGrantInput): Promise<RestPluginGrant>;
550
+ revoke(grantId: string): Promise<RestPluginGrant>;
551
+ }
552
+
399
553
  export interface ConsoleClient {
400
554
  me(signal?: AbortSignal): Promise<RestMe>;
401
555
  /** Gives up the token this client acts with, so that it is good for nothing from now on: what `console logout` does. For a token; a session has none. */
@@ -414,6 +568,7 @@ export interface ConsoleClient {
414
568
  removeWorkspacePackToken(): Promise<RestWorkspace>;
415
569
  connectWorkspacePackToken(): Promise<RestPackConnectStarted>;
416
570
  workspacePackTokenConnection(signal?: AbortSignal): Promise<RestWorkspacePackConnect>;
571
+ claimWorkspacePackTokenConnection(signal?: AbortSignal): Promise<RestWorkspacePackConnect>;
417
572
  cancelWorkspacePackTokenConnection(): Promise<void>;
418
573
  /** The people the asker may see: everyone, or those they share a project with (ADR-0035). */
419
574
  people(signal?: AbortSignal): Promise<RestPeople>;
@@ -434,8 +589,14 @@ export interface ConsoleClient {
434
589
  createProject(input: RestProjectInput): Promise<RestProject>;
435
590
  /** The board of one project. */
436
591
  project(key: string): ProjectClient;
592
+ /** The plugins the asker may know of: the enabled ones, every one for an administrator. */
593
+ plugins(signal?: AbortSignal): Promise<RestPlugins>;
594
+ /** One plugin, by its key. */
595
+ plugin(key: string): PluginClient;
437
596
  /** The workspace's own events, the ones about no project, after event number `after`. */
438
597
  events(after: number, signal?: AbortSignal): Promise<RestEvents>;
598
+ /** Where a picture from elsewhere is read through the console (ADR-0048), given the https URL that names it. */
599
+ pictureUrl(url: string): string;
439
600
  /** The categories tasks are sorted into (ADR-0019), by name. */
440
601
  categories(signal?: AbortSignal): Promise<RestCategories>;
441
602
  /** The workspace's rules (ADR-0023): the console's own until an administrator writes the organization's. */
@@ -752,6 +913,7 @@ export function createClient(options: ClientOptions = {}): ConsoleClient {
752
913
  "GET",
753
914
  withQuery(at("events"), {
754
915
  after: String(after),
916
+ latest: filter.latest === true ? "true" : undefined,
755
917
  task: filter.task,
756
918
  run: filter.run,
757
919
  decision: filter.decision,
@@ -828,6 +990,14 @@ export function createClient(options: ClientOptions = {}): ConsoleClient {
828
990
  restPackConnectSchema,
829
991
  signal,
830
992
  ),
993
+ claimPackTokenConnection: (signal) =>
994
+ request(
995
+ "POST",
996
+ `${base}${at()}/pack-token/connect/claim`,
997
+ undefined,
998
+ restPackConnectSchema,
999
+ signal,
1000
+ ),
831
1001
  cancelPackTokenConnection: () =>
832
1002
  request("DELETE", `${base}${at()}/pack-token/connect`, undefined, undefined),
833
1003
  rules: (signal) =>
@@ -895,6 +1065,147 @@ export function createClient(options: ClientOptions = {}): ConsoleClient {
895
1065
  };
896
1066
  };
897
1067
 
1068
+ /** A schema that takes whatever JSON came: a query's own answer. */
1069
+ const anything: Schema<unknown> = { safeParse: (data) => ({ success: true, data }) };
1070
+
1071
+ const pluginClient = (key: string): PluginClient => {
1072
+ const at = (...parts: readonly string[]) => pluginPath(key, ...parts);
1073
+ const skillsQuery = (part: string, params: Record<string, string | undefined>): string =>
1074
+ withQuery(at("skills", part), params);
1075
+ const queryUrl = (
1076
+ name: string,
1077
+ args: Readonly<Record<string, unknown>>,
1078
+ options: { readonly project?: string | undefined } = {},
1079
+ ): string =>
1080
+ withQuery(at("queries", name), { args: JSON.stringify(args), project: options.project });
1081
+ return {
1082
+ key,
1083
+ get: (signal) => request("GET", `${base}${at()}`, undefined, restPluginDetailSchema, signal),
1084
+ invoke: (input) =>
1085
+ request("POST", `${base}${at("invoke")}`, input, restPluginInvocationSchema),
1086
+ command: (name, input) =>
1087
+ request("POST", `${base}${at("commands", name)}`, input, restPluginInvocationSchema),
1088
+ query: (name, args, options = {}, signal) =>
1089
+ request("GET", queryUrl(name, args, options), undefined, anything, signal),
1090
+ queryUrl,
1091
+ upload: (input) =>
1092
+ request("POST", `${base}${at("files")}`, formOf(input), restPluginFileHandleSchema),
1093
+ records: (collection, filter = {}, signal) =>
1094
+ request(
1095
+ "GET",
1096
+ withQuery(at("records", collection), {
1097
+ where: filter.where === undefined ? undefined : JSON.stringify(filter.where),
1098
+ order: filter.order,
1099
+ limit: number(filter.limit),
1100
+ after: filter.after,
1101
+ q: filter.q,
1102
+ project: filter.project,
1103
+ archived: flag(filter.archived),
1104
+ }),
1105
+ undefined,
1106
+ restPluginRecordPageSchema,
1107
+ signal,
1108
+ ),
1109
+ record: (collection, id, version, signal) =>
1110
+ request(
1111
+ "GET",
1112
+ withQuery(at("records", collection, id), { version: number(version) }),
1113
+ undefined,
1114
+ restPluginRecordReadSchema,
1115
+ signal,
1116
+ ),
1117
+ recordVersions: (collection, id, signal) =>
1118
+ request(
1119
+ "GET",
1120
+ `${base}${at("records", collection, id, "versions")}`,
1121
+ undefined,
1122
+ restPluginRecordVersionsSchema,
1123
+ signal,
1124
+ ),
1125
+ fileUrl: (fileId) => `${base}${at("files", fileId)}`,
1126
+ readFile: (fileId, signal) => read(`${base}${at("files", fileId)}`, fileId, signal),
1127
+ frameUrl: (options) =>
1128
+ withQuery(at("frame"), {
1129
+ page: options.page,
1130
+ widget: options.widget,
1131
+ project: options.project,
1132
+ }),
1133
+ pluginEvents: (page, signal) =>
1134
+ request(
1135
+ "GET",
1136
+ withQuery(at("plugin-events"), { after: String(page.after), limit: String(page.limit) }),
1137
+ undefined,
1138
+ restPluginEventsSchema,
1139
+ signal,
1140
+ ),
1141
+ purge: (confirm) => request("POST", `${base}${at("purge")}`, { confirm }, undefined),
1142
+ skills: (signal) =>
1143
+ request("GET", `${base}${at("skills")}`, undefined, restSkillsSchema, signal),
1144
+ skill: (path, cursor, signal) =>
1145
+ request(
1146
+ "GET",
1147
+ skillsQuery("one", { path, cursor }),
1148
+ undefined,
1149
+ restSkillLoadSchema,
1150
+ signal,
1151
+ ),
1152
+ skillFile: (path, page = {}, signal) =>
1153
+ request(
1154
+ "GET",
1155
+ skillsQuery("file", { path, offset: number(page.offset), limit: number(page.limit) }),
1156
+ undefined,
1157
+ restSkillFileSchema,
1158
+ signal,
1159
+ ),
1160
+ skillBrowse: (page = {}, signal) =>
1161
+ request(
1162
+ "GET",
1163
+ skillsQuery("browse", {
1164
+ path: page.path,
1165
+ cursor: page.cursor,
1166
+ limit: number(page.limit),
1167
+ }),
1168
+ undefined,
1169
+ restSkillBrowseSchema,
1170
+ signal,
1171
+ ),
1172
+ skillFind: (query, options = {}, signal) =>
1173
+ request(
1174
+ "GET",
1175
+ skillsQuery("find", {
1176
+ query,
1177
+ path: options.path,
1178
+ cursor: options.cursor,
1179
+ limit: number(options.limit),
1180
+ }),
1181
+ undefined,
1182
+ restSkillFindSchema,
1183
+ signal,
1184
+ ),
1185
+ events: (page = {}, signal) =>
1186
+ request(
1187
+ "GET",
1188
+ withQuery(at("events"), { limit: number(page.limit), offset: number(page.offset) }),
1189
+ undefined,
1190
+ restEventsSchema,
1191
+ signal,
1192
+ ),
1193
+ enable: (input) => request("POST", `${base}${at("enable")}`, input, restPluginDetailSchema),
1194
+ disable: () => request("POST", `${base}${at("disable")}`, undefined, restPluginDetailSchema),
1195
+ updateSettings: (patch) =>
1196
+ request("PATCH", `${base}${at("settings")}`, patch, restPluginDetailSchema),
1197
+ setSecret: (name, value) =>
1198
+ request("PUT", `${base}${at("secrets", name)}`, { value }, undefined),
1199
+ removeSecret: (name) =>
1200
+ request("DELETE", `${base}${at("secrets", name)}`, undefined, undefined),
1201
+ grants: (signal) =>
1202
+ request("GET", `${base}${at("grants")}`, undefined, restPluginGrantsSchema, signal),
1203
+ grant: (input) => request("POST", `${base}${at("grants")}`, input, restPluginGrantSchema),
1204
+ revoke: (grantId) =>
1205
+ request("DELETE", `${base}${at("grants", grantId)}`, undefined, restPluginGrantSchema),
1206
+ };
1207
+ };
1208
+
898
1209
  return {
899
1210
  me: (signal) => request("GET", `${base}${REST_ROUTES.me}`, undefined, restMeSchema, signal),
900
1211
  giveUpToken: () => request("DELETE", `${base}${REST_ROUTES.meToken}`, undefined, undefined),
@@ -931,6 +1242,14 @@ export function createClient(options: ClientOptions = {}): ConsoleClient {
931
1242
  restWorkspacePackConnectSchema,
932
1243
  signal,
933
1244
  ),
1245
+ claimWorkspacePackTokenConnection: (signal) =>
1246
+ request(
1247
+ "POST",
1248
+ `${base}${REST_ROUTES.workspacePackToken}/connect/claim`,
1249
+ undefined,
1250
+ restWorkspacePackConnectSchema,
1251
+ signal,
1252
+ ),
934
1253
  cancelWorkspacePackTokenConnection: () =>
935
1254
  request("DELETE", `${base}${REST_ROUTES.workspacePackToken}/connect`, undefined, undefined),
936
1255
  updatePerson: (id, patch) =>
@@ -944,6 +1263,9 @@ export function createClient(options: ClientOptions = {}): ConsoleClient {
944
1263
  createProject: (input) =>
945
1264
  request("POST", `${base}${REST_ROUTES.projects}`, input, restProjectSchema),
946
1265
  project: projectClient,
1266
+ plugins: (signal) =>
1267
+ request("GET", `${base}${REST_ROUTES.plugins}`, undefined, restPluginsSchema, signal),
1268
+ plugin: pluginClient,
947
1269
  events: (after, signal) =>
948
1270
  request(
949
1271
  "GET",
@@ -952,6 +1274,7 @@ export function createClient(options: ClientOptions = {}): ConsoleClient {
952
1274
  restEventsSchema,
953
1275
  signal,
954
1276
  ),
1277
+ pictureUrl: (url) => withQuery(REST_ROUTES.pictures, { src: url }),
955
1278
  categories: (signal) =>
956
1279
  request("GET", `${base}${REST_ROUTES.categories}`, undefined, restCategoriesSchema, signal),
957
1280
  rules: (signal) =>
package/src/compat.ts CHANGED
@@ -113,7 +113,7 @@ export interface ClientPolicy {
113
113
  * last changed what the command reads or sends (ADR-0038), whose changelog entry begins with
114
114
  * `Breaking:`; `pnpm check` holds it to a version the package has, or is about to have.
115
115
  */
116
- export const CLIENT_POLICY: ClientPolicy = { atLeast: "0.4.0" };
116
+ export const CLIENT_POLICY: ClientPolicy = { atLeast: "0.4.2" };
117
117
 
118
118
  /**
119
119
  * The oldest console this command works with (ADR-0038): raised by the release that last
@@ -122,7 +122,7 @@ export const CLIENT_POLICY: ClientPolicy = { atLeast: "0.4.0" };
122
122
  * itself, naming the command to install instead; `pnpm check` holds it as it holds the
123
123
  * console's floor.
124
124
  */
125
- export const CONSOLE_AT_LEAST = "0.4.0";
125
+ export const CONSOLE_AT_LEAST = "0.4.2";
126
126
 
127
127
  /** `common` from `atLeast` on, where that floor is within it; `common` as it is otherwise. */
128
128
  function narrowed(common: VersionRange, atLeast: string | undefined): VersionRange {
@@ -80,7 +80,28 @@ const HOLD_MS = 300;
80
80
  const HOLD_SLACK = 8;
81
81
  /** How near the pointer comes to the board's edge before the board scrolls to show more columns. */
82
82
  const EDGE = 48;
83
- const EDGE_STEP = 12;
83
+ /** How fast the board scrolls, in pixels a second, with the pointer at the edge itself; slower further in. */
84
+ const EDGE_SPEED = 720;
85
+ /** The most a frame counts for, so that a tab that was away does not leap when it is back. */
86
+ const FRAME_AT_MOST_MS = 64;
87
+
88
+ /**
89
+ * How far the board scrolls in one frame for a pointer at `x`, the board spanning `left` to
90
+ * `right` on the screen: nothing away from the edges; towards the edge the pointer is near, the
91
+ * nearer the faster, and no faster beyond it. Asked every frame while a card is held, not only
92
+ * when the pointer moves, so that a card held still at the edge carries the board on, as an icon
93
+ * held at the edge of a phone's screen turns the page. Negative is leftwards.
94
+ */
95
+ export function edgeScrollOf(x: number, left: number, right: number, elapsedMs: number): number {
96
+ const ms = Math.min(Math.max(elapsedMs, 0), FRAME_AT_MOST_MS);
97
+ const depth = (distance: number) => Math.min(1, Math.max(0, (EDGE - distance) / EDGE));
98
+ const leftward = depth(x - left);
99
+ const rightward = depth(right - x);
100
+ if (leftward === 0 && rightward === 0) {
101
+ return 0;
102
+ }
103
+ return ((leftward > rightward ? -leftward : rightward) * EDGE_SPEED * ms) / 1000;
104
+ }
84
105
 
85
106
  /** The state a column stands for, read off its `data-column`; nothing for a value that is not one. */
86
107
  export function columnStateOf(value: string | null | undefined): TaskState | undefined {
@@ -422,10 +443,10 @@ export function Board(props: BoardProps) {
422
443
  }, []);
423
444
 
424
445
  // While a card is held, the pointer is followed over the whole document: the column under it
425
- // is marked, the board scrolls when the pointer nears its edge, and the release moves the task.
426
- // A finger's hold is timed here, and once the card has come loose the page no longer scrolls
427
- // under the finger, nor asks for the menu a long press would otherwise bring up; the click that
428
- // follows a drag opens nothing.
446
+ // is marked, the board scrolls while the pointer is near its edge, moving or still, and the
447
+ // release moves the task. A finger's hold is timed here, and once the card has come loose the
448
+ // page no longer scrolls under the finger, nor asks for the menu a long press would otherwise
449
+ // bring up; the click that follows a drag opens nothing.
429
450
  const held = drag !== null;
430
451
  useEffect(() => {
431
452
  if (!held) {
@@ -435,8 +456,42 @@ export function Board(props: BoardProps) {
435
456
  columnStateOf(
436
457
  document.elementFromPoint(x, y)?.closest<HTMLElement>("[data-column]")?.dataset.column,
437
458
  );
459
+ // Where the pointer last was, for the frames between its moves.
460
+ const pointer = { x: current.current?.x ?? 0, y: current.current?.y ?? 0 };
461
+ // The board is carried on every frame the loose card is near its edge; what a frame's share
462
+ // comes to short of a whole pixel is kept for the next, since a scroll position holds whole
463
+ // ones, and the column under the pointer is marked again where the board moved under it.
464
+ let frame = 0;
465
+ let at: number | undefined;
466
+ let rest = 0;
467
+ const carry = (now: number) => {
468
+ frame = window.requestAnimationFrame(carry);
469
+ const elapsed = at === undefined ? 0 : now - at;
470
+ at = now;
471
+ const element = board.current;
472
+ if (element === null || !loose.current) {
473
+ return;
474
+ }
475
+ const rect = element.getBoundingClientRect();
476
+ const share = edgeScrollOf(pointer.x, rect.left, rect.right, elapsed) + rest;
477
+ const whole = Math.trunc(share);
478
+ rest = share - whole;
479
+ if (whole === 0) {
480
+ return;
481
+ }
482
+ const was = element.scrollLeft;
483
+ element.scrollLeft = was + whole;
484
+ if (element.scrollLeft !== was) {
485
+ const over = columnUnder(pointer.x, pointer.y);
486
+ setDrag((before) =>
487
+ before === null || before.over === over ? before : { ...before, over },
488
+ );
489
+ }
490
+ };
438
491
  const follow = (event: PointerEvent) => {
439
492
  const { clientX: x, clientY: y } = event;
493
+ pointer.x = x;
494
+ pointer.y = y;
440
495
  setDrag((before) => {
441
496
  if (before === null) {
442
497
  return before;
@@ -455,15 +510,6 @@ export function Board(props: BoardProps) {
455
510
  }
456
511
  return { ...before, x, y, moving: true, over: columnUnder(x, y) };
457
512
  });
458
- const element = board.current;
459
- if (element !== null && loose.current) {
460
- const rect = element.getBoundingClientRect();
461
- if (x < rect.left + EDGE) {
462
- element.scrollLeft -= EDGE_STEP;
463
- } else if (x > rect.right - EDGE) {
464
- element.scrollLeft += EDGE_STEP;
465
- }
466
- }
467
513
  };
468
514
  const loosen = () => {
469
515
  setDrag((before) => {
@@ -502,6 +548,7 @@ export function Board(props: BoardProps) {
502
548
  };
503
549
  const quiet = (event: Event) => event.preventDefault();
504
550
  const timer = current.current?.hold === true ? window.setTimeout(loosen, HOLD_MS) : undefined;
551
+ frame = window.requestAnimationFrame(carry);
505
552
  window.addEventListener("pointermove", follow);
506
553
  window.addEventListener("pointerup", release);
507
554
  window.addEventListener("pointercancel", release);
@@ -511,6 +558,7 @@ export function Board(props: BoardProps) {
511
558
  if (timer !== undefined) {
512
559
  window.clearTimeout(timer);
513
560
  }
561
+ window.cancelAnimationFrame(frame);
514
562
  window.removeEventListener("pointermove", follow);
515
563
  window.removeEventListener("pointerup", release);
516
564
  window.removeEventListener("pointercancel", release);