@skillcdn/console 0.4.3 → 0.4.5

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 (268) hide show
  1. package/README.md +6 -6
  2. package/dist/api.d.ts +4 -4
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/api.js +4 -4
  5. package/dist/api.js.map +1 -1
  6. package/dist/cli/cli.d.ts.map +1 -1
  7. package/dist/cli/cli.js +255 -33
  8. package/dist/cli/cli.js.map +1 -1
  9. package/dist/cli/format.d.ts +20 -5
  10. package/dist/cli/format.d.ts.map +1 -1
  11. package/dist/cli/format.js +98 -26
  12. package/dist/cli/format.js.map +1 -1
  13. package/dist/cli/help.d.ts.map +1 -1
  14. package/dist/cli/help.js +39 -15
  15. package/dist/cli/help.js.map +1 -1
  16. package/dist/cli/plugin-check.d.ts +5 -1
  17. package/dist/cli/plugin-check.d.ts.map +1 -1
  18. package/dist/cli/plugin-check.js +23 -4
  19. package/dist/cli/plugin-check.js.map +1 -1
  20. package/dist/cli/plugin-cli.d.ts.map +1 -1
  21. package/dist/cli/plugin-cli.js +10 -1
  22. package/dist/cli/plugin-cli.js.map +1 -1
  23. package/dist/cli/plugin-init.d.ts +2 -0
  24. package/dist/cli/plugin-init.d.ts.map +1 -1
  25. package/dist/cli/plugin-init.js +6 -0
  26. package/dist/cli/plugin-init.js.map +1 -1
  27. package/dist/client.d.ts +32 -1
  28. package/dist/client.d.ts.map +1 -1
  29. package/dist/client.js +14 -2
  30. package/dist/client.js.map +1 -1
  31. package/dist/compat.js +1 -1
  32. package/dist/components/board.d.ts +8 -0
  33. package/dist/components/board.d.ts.map +1 -1
  34. package/dist/components/board.js +49 -18
  35. package/dist/components/board.js.map +1 -1
  36. package/dist/components/decision-form.d.ts +6 -3
  37. package/dist/components/decision-form.d.ts.map +1 -1
  38. package/dist/components/decision-form.js +24 -18
  39. package/dist/components/decision-form.js.map +1 -1
  40. package/dist/components/decision-list.d.ts +5 -1
  41. package/dist/components/decision-list.d.ts.map +1 -1
  42. package/dist/components/decision-list.js +25 -5
  43. package/dist/components/decision-list.js.map +1 -1
  44. package/dist/components/event-feed.d.ts.map +1 -1
  45. package/dist/components/event-feed.js +34 -8
  46. package/dist/components/event-feed.js.map +1 -1
  47. package/dist/components/goals.d.ts.map +1 -1
  48. package/dist/components/goals.js +2 -2
  49. package/dist/components/goals.js.map +1 -1
  50. package/dist/components/home.d.ts +35 -0
  51. package/dist/components/home.d.ts.map +1 -0
  52. package/dist/components/home.js +326 -0
  53. package/dist/components/home.js.map +1 -0
  54. package/dist/components/my-decisions.d.ts +20 -3
  55. package/dist/components/my-decisions.d.ts.map +1 -1
  56. package/dist/components/my-decisions.js +14 -1
  57. package/dist/components/my-decisions.js.map +1 -1
  58. package/dist/components/pack-connect.d.ts.map +1 -1
  59. package/dist/components/pack-connect.js +5 -4
  60. package/dist/components/pack-connect.js.map +1 -1
  61. package/dist/components/people.d.ts +45 -1
  62. package/dist/components/people.d.ts.map +1 -1
  63. package/dist/components/people.js +125 -4
  64. package/dist/components/people.js.map +1 -1
  65. package/dist/components/picker.d.ts +8 -0
  66. package/dist/components/picker.d.ts.map +1 -1
  67. package/dist/components/picker.js +2 -2
  68. package/dist/components/picker.js.map +1 -1
  69. package/dist/components/plugin-frame.d.ts +3 -13
  70. package/dist/components/plugin-frame.d.ts.map +1 -1
  71. package/dist/components/plugin-frame.js +75 -21
  72. package/dist/components/plugin-frame.js.map +1 -1
  73. package/dist/components/plugin-settings.d.ts +5 -1
  74. package/dist/components/plugin-settings.d.ts.map +1 -1
  75. package/dist/components/plugin-settings.js +283 -45
  76. package/dist/components/plugin-settings.js.map +1 -1
  77. package/dist/components/projects.d.ts +0 -8
  78. package/dist/components/projects.d.ts.map +1 -1
  79. package/dist/components/projects.js +4 -14
  80. package/dist/components/projects.js.map +1 -1
  81. package/dist/components/searches.d.ts +11 -0
  82. package/dist/components/searches.d.ts.map +1 -0
  83. package/dist/components/searches.js +30 -0
  84. package/dist/components/searches.js.map +1 -0
  85. package/dist/components/task-form.d.ts.map +1 -1
  86. package/dist/components/task-form.js +6 -5
  87. package/dist/components/task-form.js.map +1 -1
  88. package/dist/components/task-view.d.ts.map +1 -1
  89. package/dist/components/task-view.js +2 -1
  90. package/dist/components/task-view.js.map +1 -1
  91. package/dist/components/ui.d.ts +16 -0
  92. package/dist/components/ui.d.ts.map +1 -1
  93. package/dist/components/ui.js +15 -2
  94. package/dist/components/ui.js.map +1 -1
  95. package/dist/connect-page.d.ts.map +1 -1
  96. package/dist/connect-page.js +5 -6
  97. package/dist/connect-page.js.map +1 -1
  98. package/dist/console.css +1123 -258
  99. package/dist/console.d.ts +12 -4
  100. package/dist/console.d.ts.map +1 -1
  101. package/dist/console.js +123 -27
  102. package/dist/console.js.map +1 -1
  103. package/dist/data.d.ts +18 -3
  104. package/dist/data.d.ts.map +1 -1
  105. package/dist/data.js +41 -1
  106. package/dist/data.js.map +1 -1
  107. package/dist/docs-page.d.ts.map +1 -1
  108. package/dist/docs-page.js +6 -5
  109. package/dist/docs-page.js.map +1 -1
  110. package/dist/home-tiles.d.ts +92 -0
  111. package/dist/home-tiles.d.ts.map +1 -0
  112. package/dist/home-tiles.js +294 -0
  113. package/dist/home-tiles.js.map +1 -0
  114. package/dist/i18n/en.d.ts +264 -1
  115. package/dist/i18n/en.d.ts.map +1 -1
  116. package/dist/i18n/en.js +260 -1
  117. package/dist/i18n/en.js.map +1 -1
  118. package/dist/i18n/index.d.ts +4 -2
  119. package/dist/i18n/index.d.ts.map +1 -1
  120. package/dist/i18n/index.js +10 -11
  121. package/dist/i18n/index.js.map +1 -1
  122. package/dist/i18n/ko.d.ts.map +1 -1
  123. package/dist/i18n/ko.js +255 -4
  124. package/dist/i18n/ko.js.map +1 -1
  125. package/dist/index.d.ts +7 -5
  126. package/dist/index.d.ts.map +1 -1
  127. package/dist/index.js +7 -5
  128. package/dist/index.js.map +1 -1
  129. package/dist/limits.d.ts +12 -0
  130. package/dist/limits.d.ts.map +1 -1
  131. package/dist/limits.js +12 -0
  132. package/dist/limits.js.map +1 -1
  133. package/dist/plugin/args.d.ts +6 -1
  134. package/dist/plugin/args.d.ts.map +1 -1
  135. package/dist/plugin/args.js +16 -11
  136. package/dist/plugin/args.js.map +1 -1
  137. package/dist/plugin/bridge.d.ts +63 -3
  138. package/dist/plugin/bridge.d.ts.map +1 -1
  139. package/dist/plugin/bridge.js +54 -0
  140. package/dist/plugin/bridge.js.map +1 -1
  141. package/dist/plugin/capabilities.d.ts +7 -1
  142. package/dist/plugin/capabilities.d.ts.map +1 -1
  143. package/dist/plugin/capabilities.js +24 -1
  144. package/dist/plugin/capabilities.js.map +1 -1
  145. package/dist/plugin/context.d.ts +174 -12
  146. package/dist/plugin/context.d.ts.map +1 -1
  147. package/dist/plugin/define.d.ts +14 -1
  148. package/dist/plugin/define.d.ts.map +1 -1
  149. package/dist/plugin/define.js +18 -2
  150. package/dist/plugin/define.js.map +1 -1
  151. package/dist/plugin/limits.d.ts +9 -2
  152. package/dist/plugin/limits.d.ts.map +1 -1
  153. package/dist/plugin/limits.js +9 -2
  154. package/dist/plugin/limits.js.map +1 -1
  155. package/dist/plugin/manifest.d.ts +98 -6
  156. package/dist/plugin/manifest.d.ts.map +1 -1
  157. package/dist/plugin/manifest.js +171 -10
  158. package/dist/plugin/manifest.js.map +1 -1
  159. package/dist/plugin/rest-schemas.d.ts +120 -0
  160. package/dist/plugin/rest-schemas.d.ts.map +1 -1
  161. package/dist/plugin/rest-schemas.js +78 -5
  162. package/dist/plugin/rest-schemas.js.map +1 -1
  163. package/dist/plugin/sandbox.d.ts +4 -3
  164. package/dist/plugin/sandbox.d.ts.map +1 -1
  165. package/dist/plugin/sandbox.js +33 -6
  166. package/dist/plugin/sandbox.js.map +1 -1
  167. package/dist/plugin/semver.d.ts +26 -0
  168. package/dist/plugin/semver.d.ts.map +1 -0
  169. package/dist/plugin/semver.js +76 -0
  170. package/dist/plugin/semver.js.map +1 -0
  171. package/dist/plugin/testing.d.ts +15 -1
  172. package/dist/plugin/testing.d.ts.map +1 -1
  173. package/dist/plugin/testing.js +42 -5
  174. package/dist/plugin/testing.js.map +1 -1
  175. package/dist/plugin/web.d.ts +16 -1
  176. package/dist/plugin/web.d.ts.map +1 -1
  177. package/dist/plugin/web.js +44 -1
  178. package/dist/plugin/web.js.map +1 -1
  179. package/dist/plugin.d.ts +3 -2
  180. package/dist/plugin.d.ts.map +1 -1
  181. package/dist/plugin.js +3 -2
  182. package/dist/plugin.js.map +1 -1
  183. package/dist/router.d.ts +19 -0
  184. package/dist/router.d.ts.map +1 -1
  185. package/dist/router.js +18 -0
  186. package/dist/router.js.map +1 -1
  187. package/dist/routes.d.ts +11 -2
  188. package/dist/routes.d.ts.map +1 -1
  189. package/dist/routes.js +14 -1
  190. package/dist/routes.js.map +1 -1
  191. package/dist/rules.d.ts +2 -2
  192. package/dist/rules.js +3 -3
  193. package/dist/schemas.d.ts +864 -2
  194. package/dist/schemas.d.ts.map +1 -1
  195. package/dist/schemas.js +156 -9
  196. package/dist/schemas.js.map +1 -1
  197. package/dist/use-action.d.ts +8 -2
  198. package/dist/use-action.d.ts.map +1 -1
  199. package/dist/use-action.js +13 -6
  200. package/dist/use-action.js.map +1 -1
  201. package/dist/vocabulary.d.ts +3 -1
  202. package/dist/vocabulary.d.ts.map +1 -1
  203. package/dist/vocabulary.js +9 -0
  204. package/dist/vocabulary.js.map +1 -1
  205. package/docs/specs/cli.md +10 -6
  206. package/docs/specs/plugins.md +83 -62
  207. package/docs/specs/rest.md +34 -16
  208. package/package.json +1 -1
  209. package/skills/building-a-plugin/SKILL.md +15 -12
  210. package/skills/planning-the-board/references/setting-up.md +2 -1
  211. package/skills/working-the-board/SKILL.md +3 -3
  212. package/src/api.ts +23 -0
  213. package/src/cli/cli.ts +323 -30
  214. package/src/cli/format.ts +122 -26
  215. package/src/cli/help.ts +43 -14
  216. package/src/cli/plugin-check.ts +41 -2
  217. package/src/cli/plugin-cli.ts +10 -1
  218. package/src/cli/plugin-init.ts +8 -0
  219. package/src/client.ts +78 -0
  220. package/src/compat.ts +1 -1
  221. package/src/components/board.tsx +86 -45
  222. package/src/components/decision-form.tsx +73 -36
  223. package/src/components/decision-list.tsx +45 -13
  224. package/src/components/event-feed.tsx +40 -7
  225. package/src/components/goals.tsx +4 -1
  226. package/src/components/home.tsx +615 -0
  227. package/src/components/my-decisions.tsx +50 -5
  228. package/src/components/pack-connect.tsx +5 -4
  229. package/src/components/people.tsx +500 -61
  230. package/src/components/picker.tsx +11 -2
  231. package/src/components/plugin-frame.tsx +85 -63
  232. package/src/components/plugin-settings.tsx +735 -185
  233. package/src/components/projects.tsx +14 -69
  234. package/src/components/searches.ts +47 -0
  235. package/src/components/task-form.tsx +23 -28
  236. package/src/components/task-view.tsx +7 -0
  237. package/src/components/ui.tsx +42 -2
  238. package/src/connect-page.tsx +5 -6
  239. package/src/console.tsx +303 -64
  240. package/src/data.ts +62 -3
  241. package/src/docs-page.tsx +6 -5
  242. package/src/home-tiles.ts +438 -0
  243. package/src/i18n/en.ts +274 -0
  244. package/src/i18n/index.ts +17 -14
  245. package/src/i18n/ko.ts +267 -4
  246. package/src/index.ts +18 -5
  247. package/src/limits.ts +12 -0
  248. package/src/plugin/args.ts +16 -11
  249. package/src/plugin/bridge.ts +122 -1
  250. package/src/plugin/capabilities.ts +28 -1
  251. package/src/plugin/context.ts +186 -9
  252. package/src/plugin/define.ts +38 -1
  253. package/src/plugin/limits.ts +9 -2
  254. package/src/plugin/manifest.ts +228 -13
  255. package/src/plugin/rest-schemas.ts +89 -6
  256. package/src/plugin/sandbox.ts +36 -8
  257. package/src/plugin/semver.ts +89 -0
  258. package/src/plugin/testing.ts +63 -6
  259. package/src/plugin/web.ts +66 -0
  260. package/src/plugin.ts +19 -0
  261. package/src/router.ts +25 -0
  262. package/src/routes.ts +17 -2
  263. package/src/rules.ts +3 -3
  264. package/src/schemas.ts +197 -10
  265. package/src/styles/console.css +1123 -258
  266. package/src/use-action.ts +16 -7
  267. package/src/vocabulary.ts +10 -0
  268. package/template/plugin/console-plugin.json +3 -1
@@ -62,10 +62,18 @@ export interface PickerProps<T> {
62
62
  readonly onChange: (item: T | undefined) => void;
63
63
  /** The field's name, for the input and the list. */
64
64
  readonly label: string;
65
+ /** The input's id, where a label points at it. */
66
+ readonly id?: string | undefined;
65
67
  readonly placeholder: string;
66
68
  readonly disabled?: boolean | undefined;
67
69
  /** What the search may answer with but cannot be chosen: a task, as its own parent. */
68
70
  readonly exclude?: ((item: T) => boolean) | undefined;
71
+ /**
72
+ * A class of the picker's own beside `sc-picker`: `sc-picker-quiet`, a picker that reads as
73
+ * what it holds until the pointer or the focus is on it, a fact changed in place; or
74
+ * `sc-picker-add`, the way to add one more at the end of a row of chips, as low as a chip.
75
+ */
76
+ readonly className?: string | undefined;
69
77
  }
70
78
 
71
79
  export function Picker<T>(props: PickerProps<T>) {
@@ -204,7 +212,7 @@ export function Picker<T>(props: PickerProps<T>) {
204
212
 
205
213
  if (props.value !== undefined || props.held !== undefined) {
206
214
  return (
207
- <div className="sc-picker sc-picker-chosen">
215
+ <div className={cx("sc-picker", "sc-picker-chosen", props.className)}>
208
216
  <span className="sc-picker-value">
209
217
  {props.value === undefined ? props.held : props.render(props.value)}
210
218
  </span>
@@ -222,9 +230,10 @@ export function Picker<T>(props: PickerProps<T>) {
222
230
  }
223
231
 
224
232
  return (
225
- <div className="sc-picker">
233
+ <div className={cx("sc-picker", props.className)}>
226
234
  <input
227
235
  ref={field}
236
+ id={props.id}
228
237
  className="sc-input"
229
238
  type="text"
230
239
  role="combobox"
@@ -1,17 +1,24 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
2
  import type { ConsoleClient, PluginClient } from "../client.js";
3
+ import { firstDayOfWeek } from "../dates.js";
4
+ import type { Messages } from "../i18n/en.js";
5
+ import { useWords } from "../i18n/index.js";
6
+ import type { PluginWidgetSizeName } from "../plugin/bridge.js";
3
7
  import {
4
8
  type BridgeCall,
5
9
  type BridgeEventKind,
6
10
  type BridgeToFrame,
7
11
  isBridgePath,
8
12
  isBridgeToPage,
13
+ isOpenableFrom,
9
14
  PLUGIN_BRIDGE_VERSION,
10
15
  } from "../plugin/bridge.js";
11
- import { MAX_PLUGIN_WIDGET_HEIGHT } from "../plugin/limits.js";
12
16
  import type { Navigation } from "../router.js";
13
17
  import type { RestEvent, RestMe, RestPlugin, RestProject } from "../schemas.js";
14
18
  import { CONSOLE_TOKENS } from "../tokens.js";
19
+ import { wordsForFailure } from "../use-action.js";
20
+ import { CONSOLE_LANGUAGES } from "../vocabulary.js";
21
+ import { weekStartOf } from "./workspace.js";
15
22
 
16
23
  // A plugin's page or widget on the console's page (ADR-0054, docs/specs/plugins.md, "Pages"):
17
24
  // a sandboxed frame whose document the console serves, and the console's side of the bridge.
@@ -29,6 +36,8 @@ export interface PluginFrameProps {
29
36
  /** The page shown, or the widget; one of the two. */
30
37
  readonly page?: string | undefined;
31
38
  readonly widget?: string | undefined;
39
+ /** The size the widget has (ADR-0059), told to the frame at the start and when it changes. */
40
+ readonly size?: PluginWidgetSizeName | undefined;
32
41
  /** The project the page is on, or nothing on the workspace. */
33
42
  readonly project?: RestProject | undefined;
34
43
  /** What follows the page in the address, handed to the frame. */
@@ -127,6 +136,12 @@ export async function performBridgeCall(
127
136
  transfer: [bytes],
128
137
  };
129
138
  }
139
+ case "people.picture": {
140
+ const id = text(params.id, "id");
141
+ const picture = await plugin.personPicture(id);
142
+ const bytes = new Uint8Array(picture.bytes).buffer;
143
+ return { value: { type: picture.contentType, bytes }, transfer: [bytes] };
144
+ }
130
145
  case "files.put": {
131
146
  const name = text(params.name, "name");
132
147
  const type = text(params.type, "type");
@@ -146,6 +161,20 @@ export async function performBridgeCall(
146
161
  }
147
162
 
148
163
  /** The console's computed design tokens, as the page's root has them: what a frame is told to look like. */
164
+ /**
165
+ * Whether a person pressed something a moment ago, as the browser keeps it: a link in a frame
166
+ * gives the page this too, which is what lets the page open a tab for it; a browser without the
167
+ * API is left to decide for itself, as it does for any page.
168
+ */
169
+ function recentlyPressed(): boolean {
170
+ if (typeof navigator === "undefined") {
171
+ return false;
172
+ }
173
+ const activation = (navigator as Navigator & { userActivation?: { isActive: boolean } })
174
+ .userActivation;
175
+ return activation === undefined || activation.isActive;
176
+ }
177
+
149
178
  export function readTheme(): Record<string, string> {
150
179
  if (typeof document === "undefined" || typeof getComputedStyle === "undefined") {
151
180
  return {};
@@ -161,8 +190,14 @@ export function readTheme(): Record<string, string> {
161
190
  return theme;
162
191
  }
163
192
 
164
- /** What a failure of a call says to the frame: the console's code and words, or the console's own failure. */
165
- function errorOf(error: unknown): { readonly code: string; readonly message: string } {
193
+ /**
194
+ * What a failure of a call says to the frame: the code, with the console's own refusals in the
195
+ * person's language, a plugin's own in its own words; or the console's own failure.
196
+ */
197
+ function errorOf(
198
+ error: unknown,
199
+ pack: Messages,
200
+ ): { readonly code: string; readonly message: string } {
166
201
  if (error instanceof BridgeRefusal) {
167
202
  return { code: error.code, message: error.message };
168
203
  }
@@ -174,16 +209,20 @@ function errorOf(error: unknown): { readonly code: string; readonly message: str
174
209
  "message" in error &&
175
210
  typeof error.message === "string"
176
211
  ) {
177
- return { code: error.code, message: error.message };
212
+ return { code: error.code, message: wordsForFailure(error, pack) ?? error.message };
178
213
  }
179
214
  return { code: "plugin.failed", message: "the call failed" };
180
215
  }
181
216
 
182
217
  export function PluginFrame(props: PluginFrameProps) {
183
218
  const { plugin, page, widget, project, me, language, client, navigation, events } = props;
219
+ const pack = useWords();
220
+ const words = pack.plugins;
184
221
  const frame = useRef<HTMLIFrameElement>(null);
185
222
  const isWidget = widget !== undefined;
186
- const [height, setHeight] = useState(isWidget ? 160 : 480);
223
+ const [height, setHeight] = useState(480);
224
+ /** Whether the console served the frame nothing to show: a plugin turned off since the page read it, say. */
225
+ const [unavailable, setUnavailable] = useState(false);
187
226
  const subscribed = useRef<Set<BridgeEventKind>>(new Set());
188
227
  const ready = useRef(false);
189
228
  const lastPluginEvent = useRef(0);
@@ -199,6 +238,8 @@ export function PluginFrame(props: PluginFrameProps) {
199
238
  }),
200
239
  [scoped, page, widget, project],
201
240
  );
241
+ // biome-ignore lint/correctness/useExhaustiveDependencies: another address is another document
242
+ useEffect(() => setUnavailable(false), [src]);
202
243
 
203
244
  const post = useCallback((message: BridgeToFrame, transfer?: Transferable[]) => {
204
245
  // The frame's origin is opaque, so the target is any: the frame itself checks the sender.
@@ -220,6 +261,9 @@ export function PluginFrame(props: PluginFrameProps) {
220
261
  }
221
262
  const message = event.data;
222
263
  switch (message.t) {
264
+ case "unavailable":
265
+ setUnavailable(true);
266
+ return;
223
267
  case "ready":
224
268
  ready.current = true;
225
269
  post({
@@ -228,6 +272,7 @@ export function PluginFrame(props: PluginFrameProps) {
228
272
  plugin: { key: plugin.key, version: plugin.version },
229
273
  ...(page === undefined ? {} : { page }),
230
274
  ...(widget === undefined ? {} : { widget }),
275
+ ...(props.size === undefined ? {} : { size: props.size }),
231
276
  project: project === undefined ? null : { key: project.key, name: project.name },
232
277
  person: {
233
278
  id: me.person?.id ?? "",
@@ -236,6 +281,13 @@ export function PluginFrame(props: PluginFrameProps) {
236
281
  language,
237
282
  admin: me.person?.role === "admin",
238
283
  },
284
+ workspace: {
285
+ name: me.workspace.name,
286
+ languages: [...CONSOLE_LANGUAGES],
287
+ timeZone: me.workspace.timeZone ?? "UTC",
288
+ weekStart: weekStartOf(me.workspace),
289
+ firstDay: firstDayOfWeek(language, weekStartOf(me.workspace)),
290
+ },
239
291
  roles: plugin.roles,
240
292
  path,
241
293
  language,
@@ -256,7 +308,7 @@ export function PluginFrame(props: PluginFrameProps) {
256
308
  t: "result",
257
309
  id: message.id,
258
310
  ok: false,
259
- error: errorOf(error),
311
+ error: errorOf(error, pack),
260
312
  }),
261
313
  );
262
314
  return;
@@ -266,15 +318,10 @@ export function PluginFrame(props: PluginFrameProps) {
266
318
  }
267
319
  return;
268
320
  case "resize":
269
- setHeight(
270
- Math.max(
271
- 40,
272
- Math.min(
273
- Math.ceil(message.height),
274
- isWidget ? MAX_PLUGIN_WIDGET_HEIGHT : MAX_PAGE_HEIGHT,
275
- ),
276
- ),
277
- );
321
+ // A widget fills the cells its size gives it (ADR-0059); a page is as tall as it asks.
322
+ if (!isWidget) {
323
+ setHeight(Math.max(40, Math.min(Math.ceil(message.height), MAX_PAGE_HEIGHT)));
324
+ }
278
325
  return;
279
326
  case "title":
280
327
  if (!isWidget && typeof document !== "undefined") {
@@ -289,6 +336,14 @@ export function PluginFrame(props: PluginFrameProps) {
289
336
  anchor.click();
290
337
  return;
291
338
  }
339
+ case "open":
340
+ // A link a person pressed in the frame (ADR-0067): a web or a mail address, opened in a
341
+ // new tab with no opener and no referrer, and only while the press is recent, which a
342
+ // press in the frame gives this page too.
343
+ if (isOpenableFrom(message.url, window.location.origin) && recentlyPressed()) {
344
+ window.open(message.url, "_blank", "noopener,noreferrer");
345
+ }
346
+ return;
292
347
  case "subscribe":
293
348
  subscribed.current = new Set(message.kinds);
294
349
  return;
@@ -315,6 +370,14 @@ export function PluginFrame(props: PluginFrameProps) {
315
370
  }
316
371
  }, [path, post]);
317
372
 
373
+ // The person gave the widget another size: the frame lays itself out for it.
374
+ const size = props.size;
375
+ useEffect(() => {
376
+ if (ready.current && size !== undefined) {
377
+ post({ v: PLUGIN_BRIDGE_VERSION, t: "size", size });
378
+ }
379
+ }, [size, post]);
380
+
318
381
  // The plugin's commands, as the project's feed tells of them: a frame that listens hears of each.
319
382
  useEffect(() => {
320
383
  if (events === undefined) {
@@ -375,63 +438,22 @@ export function PluginFrame(props: PluginFrameProps) {
375
438
  .catch(() => undefined);
376
439
  }, [scoped]);
377
440
 
441
+ if (unavailable) {
442
+ return <p className="sc-plugin-frame-unavailable">{words.frameUnavailable}</p>;
443
+ }
378
444
  return (
379
445
  <iframe
380
446
  ref={frame}
381
- className={isWidget ? "sc-plugin-frame sc-plugin-frame-widget" : "sc-plugin-frame"}
447
+ className={
448
+ isWidget ? "sc-plugin-frame sc-plugin-frame-widget" : "sc-plugin-frame sc-plugin-frame-page"
449
+ }
382
450
  src={src}
383
451
  title={props.title}
384
452
  // As the document's own policy says: scripts, and forms firing their submit event, which
385
453
  // the policy's form-action 'none' keeps from going anywhere.
386
454
  sandbox="allow-scripts allow-forms"
387
455
  referrerPolicy="origin"
388
- style={{ height: `${height}px` }}
456
+ style={isWidget ? undefined : { height: `${height}px` }}
389
457
  />
390
458
  );
391
459
  }
392
-
393
- export interface PluginWidgetsProps {
394
- readonly plugins: readonly RestPlugin[];
395
- readonly place: "home:top" | "home:bottom";
396
- readonly me: RestMe;
397
- readonly language: string;
398
- readonly client: ConsoleClient;
399
- readonly navigation: Navigation;
400
- /** The widget's name in the person's language. */
401
- readonly nameOf: (name: Readonly<Record<string, string>>) => string;
402
- }
403
-
404
- /** The widgets at a place of the front page, in a grid of six columns: a third takes two, a half three, the full six. */
405
- export function PluginWidgets(props: PluginWidgetsProps) {
406
- const shown = props.plugins.flatMap((plugin) =>
407
- plugin.widgets
408
- .filter((widget) => widget.place === props.place)
409
- .map((widget) => ({ plugin, widget })),
410
- );
411
- if (shown.length === 0) {
412
- return null;
413
- }
414
- return (
415
- <section
416
- className={`sc-widgets ${props.place === "home:top" ? "sc-widgets-top" : "sc-widgets-bottom"}`}
417
- >
418
- {shown.map(({ plugin, widget }) => (
419
- <article
420
- key={`${plugin.key}:${widget.key}`}
421
- className={`sc-widget sc-widget-${widget.size}`}
422
- >
423
- <h2 className="sc-widget-title">{props.nameOf(widget.name)}</h2>
424
- <PluginFrame
425
- plugin={plugin}
426
- widget={widget.key}
427
- me={props.me}
428
- language={props.language}
429
- client={props.client}
430
- navigation={props.navigation}
431
- title={props.nameOf(widget.name)}
432
- />
433
- </article>
434
- ))}
435
- </section>
436
- );
437
- }