@skillcdn/console 0.4.2 → 0.4.4

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 (243) hide show
  1. package/README.md +4 -4
  2. package/dist/api.d.ts +3 -3
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/api.js +3 -3
  5. package/dist/api.js.map +1 -1
  6. package/dist/cli/cli.d.ts.map +1 -1
  7. package/dist/cli/cli.js +120 -33
  8. package/dist/cli/cli.js.map +1 -1
  9. package/dist/cli/format.d.ts +4 -2
  10. package/dist/cli/format.d.ts.map +1 -1
  11. package/dist/cli/format.js +38 -10
  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 +22 -15
  15. package/dist/cli/help.js.map +1 -1
  16. package/dist/cli/plugin-check.d.ts +10 -1
  17. package/dist/cli/plugin-check.d.ts.map +1 -1
  18. package/dist/cli/plugin-check.js +45 -4
  19. package/dist/cli/plugin-check.js.map +1 -1
  20. package/dist/cli/plugin-cli.d.ts +1 -1
  21. package/dist/cli/plugin-cli.d.ts.map +1 -1
  22. package/dist/cli/plugin-cli.js +80 -5
  23. package/dist/cli/plugin-cli.js.map +1 -1
  24. package/dist/cli/plugin-init.d.ts +19 -0
  25. package/dist/cli/plugin-init.d.ts.map +1 -1
  26. package/dist/cli/plugin-init.js +141 -3
  27. package/dist/cli/plugin-init.js.map +1 -1
  28. package/dist/client.d.ts +7 -2
  29. package/dist/client.d.ts.map +1 -1
  30. package/dist/client.js +18 -5
  31. package/dist/client.js.map +1 -1
  32. package/dist/compat.js +1 -1
  33. package/dist/components/board.d.ts +8 -0
  34. package/dist/components/board.d.ts.map +1 -1
  35. package/dist/components/board.js +3 -3
  36. package/dist/components/board.js.map +1 -1
  37. package/dist/components/comments.d.ts.map +1 -1
  38. package/dist/components/comments.js +1 -1
  39. package/dist/components/comments.js.map +1 -1
  40. package/dist/components/decision-form.d.ts +8 -1
  41. package/dist/components/decision-form.d.ts.map +1 -1
  42. package/dist/components/decision-form.js +57 -7
  43. package/dist/components/decision-form.js.map +1 -1
  44. package/dist/components/decision-list.d.ts +9 -1
  45. package/dist/components/decision-list.d.ts.map +1 -1
  46. package/dist/components/decision-list.js +53 -7
  47. package/dist/components/decision-list.js.map +1 -1
  48. package/dist/components/event-feed.d.ts.map +1 -1
  49. package/dist/components/event-feed.js +7 -1
  50. package/dist/components/event-feed.js.map +1 -1
  51. package/dist/components/goals.d.ts.map +1 -1
  52. package/dist/components/goals.js +2 -2
  53. package/dist/components/goals.js.map +1 -1
  54. package/dist/components/home.d.ts +35 -0
  55. package/dist/components/home.d.ts.map +1 -0
  56. package/dist/components/home.js +326 -0
  57. package/dist/components/home.js.map +1 -0
  58. package/dist/components/my-decisions.d.ts +2 -0
  59. package/dist/components/my-decisions.d.ts.map +1 -1
  60. package/dist/components/my-decisions.js +2 -2
  61. package/dist/components/my-decisions.js.map +1 -1
  62. package/dist/components/pack-connect.d.ts.map +1 -1
  63. package/dist/components/pack-connect.js +5 -4
  64. package/dist/components/pack-connect.js.map +1 -1
  65. package/dist/components/people.d.ts +2 -0
  66. package/dist/components/people.d.ts.map +1 -1
  67. package/dist/components/people.js +15 -2
  68. package/dist/components/people.js.map +1 -1
  69. package/dist/components/plugin-frame.d.ts +3 -12
  70. package/dist/components/plugin-frame.d.ts.map +1 -1
  71. package/dist/components/plugin-frame.js +53 -20
  72. package/dist/components/plugin-frame.js.map +1 -1
  73. package/dist/components/plugin-settings.d.ts.map +1 -1
  74. package/dist/components/plugin-settings.js +151 -26
  75. package/dist/components/plugin-settings.js.map +1 -1
  76. package/dist/components/projects.d.ts +0 -8
  77. package/dist/components/projects.d.ts.map +1 -1
  78. package/dist/components/projects.js +1 -13
  79. package/dist/components/projects.js.map +1 -1
  80. package/dist/components/task-view.d.ts +2 -1
  81. package/dist/components/task-view.d.ts.map +1 -1
  82. package/dist/components/task-view.js +2 -1
  83. package/dist/components/task-view.js.map +1 -1
  84. package/dist/components/ui.d.ts.map +1 -1
  85. package/dist/components/ui.js +5 -2
  86. package/dist/components/ui.js.map +1 -1
  87. package/dist/connect-page.d.ts.map +1 -1
  88. package/dist/connect-page.js +5 -6
  89. package/dist/connect-page.js.map +1 -1
  90. package/dist/console.css +764 -192
  91. package/dist/console.d.ts +10 -3
  92. package/dist/console.d.ts.map +1 -1
  93. package/dist/console.js +49 -28
  94. package/dist/console.js.map +1 -1
  95. package/dist/data.d.ts +6 -3
  96. package/dist/data.d.ts.map +1 -1
  97. package/dist/data.js +31 -7
  98. package/dist/data.js.map +1 -1
  99. package/dist/docs-page.d.ts.map +1 -1
  100. package/dist/docs-page.js +6 -5
  101. package/dist/docs-page.js.map +1 -1
  102. package/dist/home-tiles.d.ts +92 -0
  103. package/dist/home-tiles.d.ts.map +1 -0
  104. package/dist/home-tiles.js +294 -0
  105. package/dist/home-tiles.js.map +1 -0
  106. package/dist/i18n/en.d.ts +180 -0
  107. package/dist/i18n/en.d.ts.map +1 -1
  108. package/dist/i18n/en.js +181 -1
  109. package/dist/i18n/en.js.map +1 -1
  110. package/dist/i18n/index.d.ts +4 -2
  111. package/dist/i18n/index.d.ts.map +1 -1
  112. package/dist/i18n/index.js +10 -11
  113. package/dist/i18n/index.js.map +1 -1
  114. package/dist/i18n/ko.d.ts.map +1 -1
  115. package/dist/i18n/ko.js +176 -3
  116. package/dist/i18n/ko.js.map +1 -1
  117. package/dist/index.d.ts +4 -3
  118. package/dist/index.d.ts.map +1 -1
  119. package/dist/index.js +4 -3
  120. package/dist/index.js.map +1 -1
  121. package/dist/limits.d.ts +8 -0
  122. package/dist/limits.d.ts.map +1 -1
  123. package/dist/limits.js +8 -0
  124. package/dist/limits.js.map +1 -1
  125. package/dist/plugin/bridge.d.ts +44 -3
  126. package/dist/plugin/bridge.d.ts.map +1 -1
  127. package/dist/plugin/bridge.js +26 -0
  128. package/dist/plugin/bridge.js.map +1 -1
  129. package/dist/plugin/capabilities.d.ts +7 -1
  130. package/dist/plugin/capabilities.d.ts.map +1 -1
  131. package/dist/plugin/capabilities.js +14 -1
  132. package/dist/plugin/capabilities.js.map +1 -1
  133. package/dist/plugin/context.d.ts +24 -1
  134. package/dist/plugin/context.d.ts.map +1 -1
  135. package/dist/plugin/define.d.ts +1 -0
  136. package/dist/plugin/define.d.ts.map +1 -1
  137. package/dist/plugin/define.js +1 -1
  138. package/dist/plugin/define.js.map +1 -1
  139. package/dist/plugin/limits.d.ts +5 -2
  140. package/dist/plugin/limits.d.ts.map +1 -1
  141. package/dist/plugin/limits.js +5 -2
  142. package/dist/plugin/limits.js.map +1 -1
  143. package/dist/plugin/manifest.d.ts +83 -6
  144. package/dist/plugin/manifest.d.ts.map +1 -1
  145. package/dist/plugin/manifest.js +111 -7
  146. package/dist/plugin/manifest.js.map +1 -1
  147. package/dist/plugin/rest-schemas.d.ts +76 -0
  148. package/dist/plugin/rest-schemas.d.ts.map +1 -1
  149. package/dist/plugin/rest-schemas.js +23 -2
  150. package/dist/plugin/rest-schemas.js.map +1 -1
  151. package/dist/plugin/sandbox.d.ts +4 -3
  152. package/dist/plugin/sandbox.d.ts.map +1 -1
  153. package/dist/plugin/sandbox.js +33 -6
  154. package/dist/plugin/sandbox.js.map +1 -1
  155. package/dist/plugin/semver.d.ts +26 -0
  156. package/dist/plugin/semver.d.ts.map +1 -0
  157. package/dist/plugin/semver.js +76 -0
  158. package/dist/plugin/semver.js.map +1 -0
  159. package/dist/plugin/testing.d.ts.map +1 -1
  160. package/dist/plugin/testing.js +2 -1
  161. package/dist/plugin/testing.js.map +1 -1
  162. package/dist/plugin/web.d.ts +10 -1
  163. package/dist/plugin/web.d.ts.map +1 -1
  164. package/dist/plugin/web.js +34 -0
  165. package/dist/plugin/web.js.map +1 -1
  166. package/dist/plugin.d.ts +3 -2
  167. package/dist/plugin.d.ts.map +1 -1
  168. package/dist/plugin.js +3 -2
  169. package/dist/plugin.js.map +1 -1
  170. package/dist/rules.d.ts +2 -2
  171. package/dist/rules.js +3 -3
  172. package/dist/schemas.d.ts +574 -11
  173. package/dist/schemas.d.ts.map +1 -1
  174. package/dist/schemas.js +154 -25
  175. package/dist/schemas.js.map +1 -1
  176. package/dist/use-action.d.ts +8 -2
  177. package/dist/use-action.d.ts.map +1 -1
  178. package/dist/use-action.js +13 -6
  179. package/dist/use-action.js.map +1 -1
  180. package/dist/vocabulary.d.ts +3 -1
  181. package/dist/vocabulary.d.ts.map +1 -1
  182. package/dist/vocabulary.js +3 -0
  183. package/dist/vocabulary.js.map +1 -1
  184. package/docs/specs/cli.md +4 -3
  185. package/docs/specs/plugins.md +49 -35
  186. package/docs/specs/rest.md +20 -18
  187. package/package.json +7 -7
  188. package/skills/building-a-plugin/SKILL.md +10 -9
  189. package/skills/planning-the-board/references/setting-up.md +2 -1
  190. package/skills/working-the-board/SKILL.md +3 -3
  191. package/src/api.ts +10 -0
  192. package/src/cli/cli.ts +162 -39
  193. package/src/cli/format.ts +54 -10
  194. package/src/cli/help.ts +23 -14
  195. package/src/cli/plugin-check.ts +68 -2
  196. package/src/cli/plugin-cli.ts +104 -6
  197. package/src/cli/plugin-init.ts +178 -3
  198. package/src/client.ts +36 -9
  199. package/src/compat.ts +1 -1
  200. package/src/components/board.tsx +3 -3
  201. package/src/components/comments.tsx +1 -0
  202. package/src/components/decision-form.tsx +166 -29
  203. package/src/components/decision-list.tsx +116 -15
  204. package/src/components/event-feed.tsx +7 -1
  205. package/src/components/goals.tsx +4 -1
  206. package/src/components/home.tsx +615 -0
  207. package/src/components/my-decisions.tsx +4 -1
  208. package/src/components/pack-connect.tsx +5 -4
  209. package/src/components/people.tsx +136 -54
  210. package/src/components/plugin-frame.tsx +62 -63
  211. package/src/components/plugin-settings.tsx +428 -87
  212. package/src/components/projects.tsx +0 -55
  213. package/src/components/task-view.tsx +9 -1
  214. package/src/components/ui.tsx +6 -2
  215. package/src/connect-page.tsx +5 -6
  216. package/src/console.tsx +98 -42
  217. package/src/data.ts +41 -11
  218. package/src/docs-page.tsx +6 -5
  219. package/src/home-tiles.ts +438 -0
  220. package/src/i18n/en.ts +185 -0
  221. package/src/i18n/index.ts +17 -14
  222. package/src/i18n/ko.ts +181 -3
  223. package/src/index.ts +7 -5
  224. package/src/limits.ts +8 -0
  225. package/src/plugin/bridge.ts +77 -1
  226. package/src/plugin/capabilities.ts +17 -1
  227. package/src/plugin/context.ts +24 -2
  228. package/src/plugin/define.ts +4 -1
  229. package/src/plugin/limits.ts +5 -2
  230. package/src/plugin/manifest.ts +154 -10
  231. package/src/plugin/rest-schemas.ts +27 -1
  232. package/src/plugin/sandbox.ts +36 -8
  233. package/src/plugin/semver.ts +89 -0
  234. package/src/plugin/testing.ts +2 -1
  235. package/src/plugin/web.ts +49 -0
  236. package/src/plugin.ts +18 -0
  237. package/src/rules.ts +3 -3
  238. package/src/schemas.ts +190 -28
  239. package/src/styles/console.css +764 -192
  240. package/src/use-action.ts +16 -7
  241. package/src/vocabulary.ts +4 -0
  242. package/template/plugin/console-plugin.json +3 -1
  243. package/template/plugin/package.json +3 -3
@@ -14,7 +14,7 @@ import {
14
14
  import { useWords } from "../i18n/index.js";
15
15
  import { MAX_LOGIN_LENGTH } from "../limits.js";
16
16
  import { PERSON_ROLES } from "../vocabulary.js";
17
- import { Badge, Button, PersonChip, personName, RoleBadge, Time } from "./ui.js";
17
+ import { Avatar, Badge, Button, PersonChip, personName, RoleBadge, Time, Workers } from "./ui.js";
18
18
 
19
19
  // The people of the board, with what each may do and whether they are in (ADR-0022): those in,
20
20
  // the logins let in that nobody has signed in with yet, and those deactivated. An administrator
@@ -37,6 +37,8 @@ export interface PeopleListProps {
37
37
  * with it.
38
38
  */
39
39
  readonly onChangeStatus?: ((person: RestPerson, status: PersonStatus) => void) | undefined;
40
+ /** Called with the manager an administrator chose for the person, or null for none. Left out, managers are read here, not changed. */
41
+ readonly onChangeManager?: ((person: RestPerson, managerId: string | null) => void) | undefined;
40
42
  /** Called when an administrator takes a listing away. Left out, listings are read here. */
41
43
  readonly onUnlist?: ((listing: RestListing) => void) | undefined;
42
44
  /** Where a person's work is seen; given to an administrator, or an owner of a project. */
@@ -57,6 +59,12 @@ export function PeopleList(props: PeopleListProps) {
57
59
  person.source !== "organization" &&
58
60
  props.me?.id !== person.id;
59
61
  const organization = props.organization ?? undefined;
62
+ const byId = new Map(props.people.map((person) => [person.id, person]));
63
+ /** The person's manager, where they have one that can be named here. */
64
+ const managerOf = (person: RestPerson): RestPerson | undefined =>
65
+ person.managerId === null || person.managerId === undefined
66
+ ? undefined
67
+ : byId.get(person.managerId);
60
68
  /** What let the person in, by its name: the organization, the domain of their login, or an administrator. */
61
69
  const noteOf = (person: RestPerson): string => {
62
70
  if (person.source === "organization") {
@@ -85,59 +93,133 @@ export function PeopleList(props: PeopleListProps) {
85
93
  </p>
86
94
  )}
87
95
  <ul className="sc-people" aria-label={people.list}>
88
- {active.map((person) => (
89
- <li key={person.id} className="sc-person-row">
90
- <PersonChip person={person} size="md" />
91
- {props.me?.id === person.id && (
92
- <span className="sc-person-you">{words.common.you}</span>
93
- )}
94
- <span className="sc-person-name sc-muted">
95
- {person.name !== null && <>{person.login} · </>}
96
- <span className="sc-person-note">{noteOf(person)}</span>
97
- </span>
98
- {props.onChangeRole === undefined ? (
99
- <RoleBadge role={person.role} />
100
- ) : (
101
- <label className="sc-field sc-field-inline">
102
- <span className="sc-visually-hidden">{people.roleOf(personName(person))}</span>
103
- <select
104
- className="sc-input"
105
- value={person.role}
106
- disabled={props.busy === true}
107
- onChange={(event) =>
108
- props.onChangeRole?.(person, event.target.value as PersonRole)
109
- }
110
- >
111
- {PERSON_ROLES.map((role) => (
112
- <option key={role} value={role}>
113
- {words.vocabulary.role[role]}
114
- </option>
115
- ))}
116
- </select>
117
- </label>
118
- )}
119
- {props.workHref !== undefined && (
120
- <a className="sc-button sc-button-ghost sc-button-sm" href={props.workHref(person)}>
121
- {people.work}
122
- </a>
123
- )}
124
- {props.agentsHref !== undefined && (
125
- <a className="sc-button sc-button-ghost sc-button-sm" href={props.agentsHref(person)}>
126
- {people.agents}
127
- </a>
128
- )}
129
- {mayChange(person) && (
130
- <Button
131
- variant="ghost"
132
- size="sm"
133
- disabled={props.busy === true}
134
- onClick={() => props.onChangeStatus?.(person, "deactivated")}
135
- >
136
- {people.deactivate}
137
- </Button>
138
- )}
139
- </li>
140
- ))}
96
+ {active.map((person) => {
97
+ const manager = managerOf(person);
98
+ const team = active.filter((other) => other.managerId === person.id);
99
+ const hasWays =
100
+ props.workHref !== undefined || props.agentsHref !== undefined || mayChange(person);
101
+ return (
102
+ <li key={person.id} className="sc-person-card">
103
+ <div className="sc-person-card-head">
104
+ <Avatar person={person} size="lg" />
105
+ <div className="sc-person-card-who">
106
+ <span className="sc-person-card-name">
107
+ {personName(person)}
108
+ {props.me?.id === person.id && (
109
+ <span className="sc-person-you">{words.common.you}</span>
110
+ )}
111
+ </span>
112
+ <span className="sc-person-card-note">
113
+ {person.name !== null && <>{person.login} · </>}
114
+ {noteOf(person)}
115
+ </span>
116
+ </div>
117
+ </div>
118
+ <dl className="sc-person-facts">
119
+ <div className="sc-person-fact">
120
+ <dt>{people.role}</dt>
121
+ <dd>
122
+ {props.onChangeRole === undefined ? (
123
+ <RoleBadge role={person.role} />
124
+ ) : (
125
+ <select
126
+ className="sc-input sc-input-quiet"
127
+ aria-label={people.roleOf(personName(person))}
128
+ value={person.role}
129
+ disabled={props.busy === true}
130
+ onChange={(event) =>
131
+ props.onChangeRole?.(person, event.target.value as PersonRole)
132
+ }
133
+ >
134
+ {PERSON_ROLES.map((role) => (
135
+ <option key={role} value={role}>
136
+ {words.vocabulary.role[role]}
137
+ </option>
138
+ ))}
139
+ </select>
140
+ )}
141
+ </dd>
142
+ </div>
143
+ <div className="sc-person-fact">
144
+ <dt>{people.manager}</dt>
145
+ <dd>
146
+ {props.onChangeManager === undefined ? (
147
+ manager === undefined ? (
148
+ <span className="sc-muted">{people.nobody}</span>
149
+ ) : (
150
+ <PersonChip person={manager} />
151
+ )
152
+ ) : (
153
+ <select
154
+ className="sc-input sc-input-quiet"
155
+ aria-label={people.managerOf(personName(person))}
156
+ value={person.managerId ?? ""}
157
+ disabled={props.busy === true}
158
+ onChange={(event) =>
159
+ props.onChangeManager?.(
160
+ person,
161
+ event.target.value === "" ? null : event.target.value,
162
+ )
163
+ }
164
+ >
165
+ <option value="">{people.nobody}</option>
166
+ {props.people
167
+ .filter(
168
+ (candidate) =>
169
+ candidate.id !== person.id &&
170
+ (candidate.status === "active" || candidate.id === person.managerId),
171
+ )
172
+ .map((candidate) => (
173
+ <option key={candidate.id} value={candidate.id}>
174
+ {personName(candidate)}
175
+ </option>
176
+ ))}
177
+ </select>
178
+ )}
179
+ </dd>
180
+ </div>
181
+ {team.length > 0 && (
182
+ <div className="sc-person-fact">
183
+ <dt>{people.team}</dt>
184
+ <dd>
185
+ <Workers workers={team} />
186
+ </dd>
187
+ </div>
188
+ )}
189
+ </dl>
190
+ {hasWays && (
191
+ <div className="sc-person-card-ways">
192
+ {props.workHref !== undefined && (
193
+ <a
194
+ className="sc-button sc-button-ghost sc-button-sm"
195
+ href={props.workHref(person)}
196
+ >
197
+ {people.work}
198
+ </a>
199
+ )}
200
+ {props.agentsHref !== undefined && (
201
+ <a
202
+ className="sc-button sc-button-ghost sc-button-sm"
203
+ href={props.agentsHref(person)}
204
+ >
205
+ {people.agents}
206
+ </a>
207
+ )}
208
+ {mayChange(person) && (
209
+ <Button
210
+ variant="ghost"
211
+ size="sm"
212
+ disabled={props.busy === true}
213
+ onClick={() => props.onChangeStatus?.(person, "deactivated")}
214
+ >
215
+ {people.deactivate}
216
+ </Button>
217
+ )}
218
+ </div>
219
+ )}
220
+ </li>
221
+ );
222
+ })}
141
223
  </ul>
142
224
  {listed.length > 0 && (
143
225
  <section className="sc-people-section" aria-label={people.notSignedIn}>
@@ -1,5 +1,9 @@
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,
@@ -8,10 +12,12 @@ import {
8
12
  isBridgeToPage,
9
13
  PLUGIN_BRIDGE_VERSION,
10
14
  } from "../plugin/bridge.js";
11
- import { MAX_PLUGIN_WIDGET_HEIGHT } from "../plugin/limits.js";
12
15
  import type { Navigation } from "../router.js";
13
16
  import type { RestEvent, RestMe, RestPlugin, RestProject } from "../schemas.js";
14
17
  import { CONSOLE_TOKENS } from "../tokens.js";
18
+ import { wordsForFailure } from "../use-action.js";
19
+ import { CONSOLE_LANGUAGES } from "../vocabulary.js";
20
+ import { weekStartOf } from "./workspace.js";
15
21
 
16
22
  // A plugin's page or widget on the console's page (ADR-0054, docs/specs/plugins.md, "Pages"):
17
23
  // a sandboxed frame whose document the console serves, and the console's side of the bridge.
@@ -29,6 +35,8 @@ export interface PluginFrameProps {
29
35
  /** The page shown, or the widget; one of the two. */
30
36
  readonly page?: string | undefined;
31
37
  readonly widget?: string | undefined;
38
+ /** The size the widget has (ADR-0059), told to the frame at the start and when it changes. */
39
+ readonly size?: PluginWidgetSizeName | undefined;
32
40
  /** The project the page is on, or nothing on the workspace. */
33
41
  readonly project?: RestProject | undefined;
34
42
  /** What follows the page in the address, handed to the frame. */
@@ -127,6 +135,12 @@ export async function performBridgeCall(
127
135
  transfer: [bytes],
128
136
  };
129
137
  }
138
+ case "people.picture": {
139
+ const id = text(params.id, "id");
140
+ const picture = await plugin.personPicture(id);
141
+ const bytes = new Uint8Array(picture.bytes).buffer;
142
+ return { value: { type: picture.contentType, bytes }, transfer: [bytes] };
143
+ }
130
144
  case "files.put": {
131
145
  const name = text(params.name, "name");
132
146
  const type = text(params.type, "type");
@@ -161,8 +175,14 @@ export function readTheme(): Record<string, string> {
161
175
  return theme;
162
176
  }
163
177
 
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 } {
178
+ /**
179
+ * What a failure of a call says to the frame: the code, with the console's own refusals in the
180
+ * person's language, a plugin's own in its own words; or the console's own failure.
181
+ */
182
+ function errorOf(
183
+ error: unknown,
184
+ pack: Messages,
185
+ ): { readonly code: string; readonly message: string } {
166
186
  if (error instanceof BridgeRefusal) {
167
187
  return { code: error.code, message: error.message };
168
188
  }
@@ -174,16 +194,20 @@ function errorOf(error: unknown): { readonly code: string; readonly message: str
174
194
  "message" in error &&
175
195
  typeof error.message === "string"
176
196
  ) {
177
- return { code: error.code, message: error.message };
197
+ return { code: error.code, message: wordsForFailure(error, pack) ?? error.message };
178
198
  }
179
199
  return { code: "plugin.failed", message: "the call failed" };
180
200
  }
181
201
 
182
202
  export function PluginFrame(props: PluginFrameProps) {
183
203
  const { plugin, page, widget, project, me, language, client, navigation, events } = props;
204
+ const pack = useWords();
205
+ const words = pack.plugins;
184
206
  const frame = useRef<HTMLIFrameElement>(null);
185
207
  const isWidget = widget !== undefined;
186
- const [height, setHeight] = useState(isWidget ? 160 : 480);
208
+ const [height, setHeight] = useState(480);
209
+ /** Whether the console served the frame nothing to show: a plugin turned off since the page read it, say. */
210
+ const [unavailable, setUnavailable] = useState(false);
187
211
  const subscribed = useRef<Set<BridgeEventKind>>(new Set());
188
212
  const ready = useRef(false);
189
213
  const lastPluginEvent = useRef(0);
@@ -199,6 +223,8 @@ export function PluginFrame(props: PluginFrameProps) {
199
223
  }),
200
224
  [scoped, page, widget, project],
201
225
  );
226
+ // biome-ignore lint/correctness/useExhaustiveDependencies: another address is another document
227
+ useEffect(() => setUnavailable(false), [src]);
202
228
 
203
229
  const post = useCallback((message: BridgeToFrame, transfer?: Transferable[]) => {
204
230
  // The frame's origin is opaque, so the target is any: the frame itself checks the sender.
@@ -220,6 +246,9 @@ export function PluginFrame(props: PluginFrameProps) {
220
246
  }
221
247
  const message = event.data;
222
248
  switch (message.t) {
249
+ case "unavailable":
250
+ setUnavailable(true);
251
+ return;
223
252
  case "ready":
224
253
  ready.current = true;
225
254
  post({
@@ -228,6 +257,7 @@ export function PluginFrame(props: PluginFrameProps) {
228
257
  plugin: { key: plugin.key, version: plugin.version },
229
258
  ...(page === undefined ? {} : { page }),
230
259
  ...(widget === undefined ? {} : { widget }),
260
+ ...(props.size === undefined ? {} : { size: props.size }),
231
261
  project: project === undefined ? null : { key: project.key, name: project.name },
232
262
  person: {
233
263
  id: me.person?.id ?? "",
@@ -236,6 +266,13 @@ export function PluginFrame(props: PluginFrameProps) {
236
266
  language,
237
267
  admin: me.person?.role === "admin",
238
268
  },
269
+ workspace: {
270
+ name: me.workspace.name,
271
+ languages: [...CONSOLE_LANGUAGES],
272
+ timeZone: me.workspace.timeZone ?? "UTC",
273
+ weekStart: weekStartOf(me.workspace),
274
+ firstDay: firstDayOfWeek(language, weekStartOf(me.workspace)),
275
+ },
239
276
  roles: plugin.roles,
240
277
  path,
241
278
  language,
@@ -256,7 +293,7 @@ export function PluginFrame(props: PluginFrameProps) {
256
293
  t: "result",
257
294
  id: message.id,
258
295
  ok: false,
259
- error: errorOf(error),
296
+ error: errorOf(error, pack),
260
297
  }),
261
298
  );
262
299
  return;
@@ -266,15 +303,10 @@ export function PluginFrame(props: PluginFrameProps) {
266
303
  }
267
304
  return;
268
305
  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
- );
306
+ // A widget fills the cells its size gives it (ADR-0059); a page is as tall as it asks.
307
+ if (!isWidget) {
308
+ setHeight(Math.max(40, Math.min(Math.ceil(message.height), MAX_PAGE_HEIGHT)));
309
+ }
278
310
  return;
279
311
  case "title":
280
312
  if (!isWidget && typeof document !== "undefined") {
@@ -315,6 +347,14 @@ export function PluginFrame(props: PluginFrameProps) {
315
347
  }
316
348
  }, [path, post]);
317
349
 
350
+ // The person gave the widget another size: the frame lays itself out for it.
351
+ const size = props.size;
352
+ useEffect(() => {
353
+ if (ready.current && size !== undefined) {
354
+ post({ v: PLUGIN_BRIDGE_VERSION, t: "size", size });
355
+ }
356
+ }, [size, post]);
357
+
318
358
  // The plugin's commands, as the project's feed tells of them: a frame that listens hears of each.
319
359
  useEffect(() => {
320
360
  if (events === undefined) {
@@ -375,63 +415,22 @@ export function PluginFrame(props: PluginFrameProps) {
375
415
  .catch(() => undefined);
376
416
  }, [scoped]);
377
417
 
418
+ if (unavailable) {
419
+ return <p className="sc-plugin-frame-unavailable">{words.frameUnavailable}</p>;
420
+ }
378
421
  return (
379
422
  <iframe
380
423
  ref={frame}
381
- className={isWidget ? "sc-plugin-frame sc-plugin-frame-widget" : "sc-plugin-frame"}
424
+ className={
425
+ isWidget ? "sc-plugin-frame sc-plugin-frame-widget" : "sc-plugin-frame sc-plugin-frame-page"
426
+ }
382
427
  src={src}
383
428
  title={props.title}
384
429
  // As the document's own policy says: scripts, and forms firing their submit event, which
385
430
  // the policy's form-action 'none' keeps from going anywhere.
386
431
  sandbox="allow-scripts allow-forms"
387
432
  referrerPolicy="origin"
388
- style={{ height: `${height}px` }}
433
+ style={isWidget ? undefined : { height: `${height}px` }}
389
434
  />
390
435
  );
391
436
  }
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
- }