@skillcdn/console 0.3.0 → 0.3.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 (170) hide show
  1. package/README.md +4 -4
  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 +5 -1
  7. package/dist/cli/cli.d.ts.map +1 -1
  8. package/dist/cli/cli.js +290 -46
  9. package/dist/cli/cli.js.map +1 -1
  10. package/dist/cli/credentials.d.ts +5 -0
  11. package/dist/cli/credentials.d.ts.map +1 -1
  12. package/dist/cli/credentials.js +2 -0
  13. package/dist/cli/credentials.js.map +1 -1
  14. package/dist/cli/directory.d.ts +7 -0
  15. package/dist/cli/directory.d.ts.map +1 -1
  16. package/dist/cli/directory.js +7 -0
  17. package/dist/cli/directory.js.map +1 -1
  18. package/dist/cli/format.d.ts +10 -1
  19. package/dist/cli/format.d.ts.map +1 -1
  20. package/dist/cli/format.js +76 -20
  21. package/dist/cli/format.js.map +1 -1
  22. package/dist/cli/help.d.ts.map +1 -1
  23. package/dist/cli/help.js +42 -23
  24. package/dist/cli/help.js.map +1 -1
  25. package/dist/cli/main.js +1 -0
  26. package/dist/cli/main.js.map +1 -1
  27. package/dist/client.d.ts +39 -9
  28. package/dist/client.d.ts.map +1 -1
  29. package/dist/client.js +24 -3
  30. package/dist/client.js.map +1 -1
  31. package/dist/compat.d.ts +28 -6
  32. package/dist/compat.d.ts.map +1 -1
  33. package/dist/compat.js +55 -14
  34. package/dist/compat.js.map +1 -1
  35. package/dist/components/board.d.ts.map +1 -1
  36. package/dist/components/board.js +6 -4
  37. package/dist/components/board.js.map +1 -1
  38. package/dist/components/calendar.d.ts +18 -8
  39. package/dist/components/calendar.d.ts.map +1 -1
  40. package/dist/components/calendar.js +39 -27
  41. package/dist/components/calendar.js.map +1 -1
  42. package/dist/components/event-feed.d.ts.map +1 -1
  43. package/dist/components/event-feed.js +2 -0
  44. package/dist/components/event-feed.js.map +1 -1
  45. package/dist/components/pack-connect.d.ts +23 -0
  46. package/dist/components/pack-connect.d.ts.map +1 -0
  47. package/dist/components/pack-connect.js +86 -0
  48. package/dist/components/pack-connect.js.map +1 -0
  49. package/dist/components/pack-token.d.ts +27 -0
  50. package/dist/components/pack-token.d.ts.map +1 -0
  51. package/dist/components/pack-token.js +11 -0
  52. package/dist/components/pack-token.js.map +1 -0
  53. package/dist/components/picker.d.ts +39 -0
  54. package/dist/components/picker.d.ts.map +1 -0
  55. package/dist/components/picker.js +155 -0
  56. package/dist/components/picker.js.map +1 -0
  57. package/dist/components/projects.d.ts +13 -0
  58. package/dist/components/projects.d.ts.map +1 -1
  59. package/dist/components/projects.js +37 -5
  60. package/dist/components/projects.js.map +1 -1
  61. package/dist/components/task-form.d.ts +18 -4
  62. package/dist/components/task-form.d.ts.map +1 -1
  63. package/dist/components/task-form.js +56 -13
  64. package/dist/components/task-form.js.map +1 -1
  65. package/dist/components/task-list.js +2 -2
  66. package/dist/components/task-list.js.map +1 -1
  67. package/dist/components/task-view.d.ts +7 -1
  68. package/dist/components/task-view.d.ts.map +1 -1
  69. package/dist/components/task-view.js +6 -5
  70. package/dist/components/task-view.js.map +1 -1
  71. package/dist/components/ui.d.ts +7 -0
  72. package/dist/components/ui.d.ts.map +1 -1
  73. package/dist/components/ui.js +11 -0
  74. package/dist/components/ui.js.map +1 -1
  75. package/dist/components/work.d.ts +3 -1
  76. package/dist/components/work.d.ts.map +1 -1
  77. package/dist/components/work.js +6 -4
  78. package/dist/components/work.js.map +1 -1
  79. package/dist/components/workspace.d.ts +46 -0
  80. package/dist/components/workspace.d.ts.map +1 -0
  81. package/dist/components/workspace.js +124 -0
  82. package/dist/components/workspace.js.map +1 -0
  83. package/dist/console.css +319 -11
  84. package/dist/console.d.ts.map +1 -1
  85. package/dist/console.js +83 -24
  86. package/dist/console.js.map +1 -1
  87. package/dist/data.d.ts +26 -1
  88. package/dist/data.d.ts.map +1 -1
  89. package/dist/data.js +59 -1
  90. package/dist/data.js.map +1 -1
  91. package/dist/dates.d.ts +12 -2
  92. package/dist/dates.d.ts.map +1 -1
  93. package/dist/dates.js +20 -2
  94. package/dist/dates.js.map +1 -1
  95. package/dist/i18n/en.d.ts +64 -3
  96. package/dist/i18n/en.d.ts.map +1 -1
  97. package/dist/i18n/en.js +81 -16
  98. package/dist/i18n/en.js.map +1 -1
  99. package/dist/i18n/ko.d.ts.map +1 -1
  100. package/dist/i18n/ko.js +77 -16
  101. package/dist/i18n/ko.js.map +1 -1
  102. package/dist/index.d.ts +6 -2
  103. package/dist/index.d.ts.map +1 -1
  104. package/dist/index.js +6 -2
  105. package/dist/index.js.map +1 -1
  106. package/dist/limits.d.ts +2 -0
  107. package/dist/limits.d.ts.map +1 -1
  108. package/dist/limits.js +2 -0
  109. package/dist/limits.js.map +1 -1
  110. package/dist/router.d.ts +4 -4
  111. package/dist/router.d.ts.map +1 -1
  112. package/dist/router.js +11 -2
  113. package/dist/router.js.map +1 -1
  114. package/dist/routes.d.ts +4 -0
  115. package/dist/routes.d.ts.map +1 -1
  116. package/dist/routes.js +4 -0
  117. package/dist/routes.js.map +1 -1
  118. package/dist/schemas.d.ts +194 -13
  119. package/dist/schemas.d.ts.map +1 -1
  120. package/dist/schemas.js +121 -21
  121. package/dist/schemas.js.map +1 -1
  122. package/dist/vocabulary.d.ts +21 -1
  123. package/dist/vocabulary.d.ts.map +1 -1
  124. package/dist/vocabulary.js +18 -0
  125. package/dist/vocabulary.js.map +1 -1
  126. package/docs/specs/cli.md +25 -20
  127. package/docs/specs/rest.md +42 -19
  128. package/package.json +2 -2
  129. package/skills/building-a-console/SKILL.md +6 -6
  130. package/skills/building-a-console/references/the-look-from-a-website.md +39 -0
  131. package/skills/deploying-a-console/SKILL.md +9 -8
  132. package/skills/deploying-a-console/references/resources.md +1 -1
  133. package/skills/working-the-board/SKILL.md +5 -5
  134. package/skills/working-the-board/references/setting-up.md +1 -1
  135. package/skills/working-the-board/references/terms.md +3 -1
  136. package/skills/working-the-board/references/the-sentence.md +1 -1
  137. package/src/api.ts +21 -0
  138. package/src/cli/cli.ts +384 -51
  139. package/src/cli/credentials.ts +7 -0
  140. package/src/cli/directory.ts +9 -0
  141. package/src/cli/format.ts +83 -20
  142. package/src/cli/help.ts +50 -23
  143. package/src/cli/main.ts +1 -0
  144. package/src/client.ts +107 -10
  145. package/src/compat.ts +63 -14
  146. package/src/components/board.tsx +10 -4
  147. package/src/components/calendar.tsx +76 -34
  148. package/src/components/event-feed.tsx +2 -0
  149. package/src/components/pack-connect.tsx +179 -0
  150. package/src/components/pack-token.tsx +74 -0
  151. package/src/components/picker.tsx +297 -0
  152. package/src/components/projects.tsx +108 -9
  153. package/src/components/task-form.tsx +149 -63
  154. package/src/components/task-list.tsx +2 -2
  155. package/src/components/task-view.tsx +23 -9
  156. package/src/components/ui.tsx +25 -0
  157. package/src/components/work.tsx +10 -5
  158. package/src/components/workspace.tsx +298 -0
  159. package/src/console.tsx +202 -32
  160. package/src/data.ts +101 -1
  161. package/src/dates.ts +23 -2
  162. package/src/i18n/en.ts +92 -16
  163. package/src/i18n/ko.ts +84 -16
  164. package/src/index.ts +32 -1
  165. package/src/limits.ts +2 -0
  166. package/src/router.ts +17 -5
  167. package/src/routes.ts +4 -0
  168. package/src/schemas.ts +140 -19
  169. package/src/styles/console.css +319 -11
  170. package/src/vocabulary.ts +28 -0
@@ -0,0 +1,298 @@
1
+ import { type FormEvent, useId, useMemo, useState } from "react";
2
+ import {
3
+ PEOPLE_VISIBILITIES,
4
+ type PeopleVisibility,
5
+ type RestWorkspace,
6
+ type RestWorkspacePatch,
7
+ WEEK_STARTS,
8
+ type WeekStart,
9
+ } from "../api.js";
10
+ import { useWords } from "../i18n/index.js";
11
+ import { Button } from "./ui.js";
12
+
13
+ // The workspace's own settings (ADR-0035), as an administrator keeps them on the Settings page:
14
+ // read by everyone, so that a member knows what the board shows them; changed on a form of
15
+ // their own, each setting a row with its words beside its control. Takes its data as props and
16
+ // nothing from the network.
17
+
18
+ /** What a workspace is set to until an administrator says otherwise, for a console that answers no setting. */
19
+ export const DEFAULT_PEOPLE_VISIBILITY: PeopleVisibility = "everyone";
20
+
21
+ /** Who sees whom, as the workspace says, or as a console older than the setting would have it. */
22
+ export function peopleVisibilityOf(workspace: RestWorkspace | undefined): PeopleVisibility {
23
+ return workspace?.peopleVisibility ?? DEFAULT_PEOPLE_VISIBILITY;
24
+ }
25
+
26
+ /** The day the calendars begin a week on, as the workspace says, or the system default. */
27
+ export function weekStartOf(workspace: RestWorkspace | undefined): WeekStart {
28
+ return workspace?.weekStart ?? "auto";
29
+ }
30
+
31
+ /** The workspace's time zone, or none. */
32
+ export function timeZoneOf(workspace: RestWorkspace | undefined): string | undefined {
33
+ return workspace?.timeZone ?? undefined;
34
+ }
35
+
36
+ /** The zones this browser knows, for the picker to offer; none where it cannot say. */
37
+ export function knownTimeZones(): readonly string[] {
38
+ try {
39
+ const intl = Intl as typeof Intl & {
40
+ readonly supportedValuesOf?: (key: "timeZone") => string[];
41
+ };
42
+ return intl.supportedValuesOf?.("timeZone") ?? [];
43
+ } catch {
44
+ return [];
45
+ }
46
+ }
47
+
48
+ /** The zone this device keeps time in, as the browser says; none where it cannot say. */
49
+ export function deviceTimeZone(): string | undefined {
50
+ try {
51
+ const zone = new Intl.DateTimeFormat().resolvedOptions().timeZone;
52
+ return zone === undefined || zone === "" ? undefined : zone;
53
+ } catch {
54
+ return undefined;
55
+ }
56
+ }
57
+
58
+ /** A zone's offset from UTC at a moment, as `UTC+09:00`; none where the browser cannot say. */
59
+ export function offsetOf(zone: string, at: Date): string | undefined {
60
+ try {
61
+ const name = new Intl.DateTimeFormat("en-US", { timeZone: zone, timeZoneName: "longOffset" })
62
+ .formatToParts(at)
63
+ .find((part) => part.type === "timeZoneName")?.value;
64
+ if (name === undefined) {
65
+ return undefined;
66
+ }
67
+ return name === "GMT" ? "UTC+00:00" : name.replace(/^GMT/, "UTC");
68
+ } catch {
69
+ return undefined;
70
+ }
71
+ }
72
+
73
+ export interface TimeZoneOption {
74
+ readonly name: string;
75
+ /** The offset now, for the person to tell zones apart by; none where the browser cannot say. */
76
+ readonly offset: string | undefined;
77
+ }
78
+
79
+ export interface TimeZoneGroup {
80
+ /** The area before the slash, `Asia` of `Asia/Seoul`; empty for a zone with none, such as `UTC`. */
81
+ readonly area: string;
82
+ readonly zones: readonly TimeZoneOption[];
83
+ }
84
+
85
+ /**
86
+ * Zones for a picker: by their area, the areas in order, each zone with its offset at `at`; the
87
+ * zones with no area first, since `UTC` is one. `extra` zones the browser does not list, the one
88
+ * set and the device's, are offered too, so that the picker always shows what is chosen.
89
+ */
90
+ export function timeZoneGroups(
91
+ zones: readonly string[],
92
+ at: Date,
93
+ extra: readonly (string | undefined)[] = [],
94
+ ): TimeZoneGroup[] {
95
+ const names = new Set(zones);
96
+ for (const name of extra) {
97
+ if (name !== undefined && name !== "") {
98
+ names.add(name);
99
+ }
100
+ }
101
+ const groups = new Map<string, TimeZoneOption[]>();
102
+ for (const name of [...names].sort((a, b) => a.localeCompare(b, "en"))) {
103
+ const slash = name.indexOf("/");
104
+ const area = slash < 0 ? "" : name.slice(0, slash);
105
+ const list = groups.get(area) ?? [];
106
+ list.push({ name, offset: offsetOf(name, at) });
107
+ groups.set(area, list);
108
+ }
109
+ return [...groups.entries()]
110
+ .sort(([a], [b]) => (a === "" ? -1 : b === "" ? 1 : a.localeCompare(b, "en")))
111
+ .map(([area, options]) => ({ area, zones: options }));
112
+ }
113
+
114
+ /** A zone as the picker shows it: its name, and its offset where known. */
115
+ const labelOf = (zone: TimeZoneOption): string =>
116
+ zone.offset === undefined ? zone.name : `${zone.name} (${zone.offset})`;
117
+
118
+ export interface WorkspaceSettingsViewProps {
119
+ readonly workspace: RestWorkspace;
120
+ }
121
+
122
+ /** The settings as they stand, one row each; where they are changed is the page's business. */
123
+ export function WorkspaceSettingsView(props: WorkspaceSettingsViewProps) {
124
+ const words = useWords();
125
+ const settings = words.settings.workspace;
126
+ const zone = timeZoneOf(props.workspace);
127
+ const device = deviceTimeZone();
128
+ const offset = zone === undefined ? undefined : offsetOf(zone, new Date());
129
+ return (
130
+ <dl className="sc-settings-list">
131
+ <div className="sc-settings-item">
132
+ <dt>{settings.peopleVisibility}</dt>
133
+ <dd>{words.vocabulary.peopleVisibility[peopleVisibilityOf(props.workspace)]}</dd>
134
+ </div>
135
+ <div className="sc-settings-item">
136
+ <dt>{settings.weekStart}</dt>
137
+ <dd>{words.vocabulary.weekStart[weekStartOf(props.workspace)]}</dd>
138
+ </div>
139
+ <div className="sc-settings-item">
140
+ <dt>{settings.timeZone}</dt>
141
+ <dd>
142
+ {zone === undefined ? (
143
+ settings.timeZoneNone
144
+ ) : (
145
+ <>
146
+ {zone}
147
+ {offset !== undefined && <span className="sc-muted"> ({offset})</span>}
148
+ </>
149
+ )}
150
+ {device !== undefined && device !== zone && (
151
+ <span className="sc-settings-note">{settings.timeZoneDevice(device)}</span>
152
+ )}
153
+ </dd>
154
+ </div>
155
+ </dl>
156
+ );
157
+ }
158
+
159
+ export interface WorkspaceSettingsFormProps {
160
+ readonly workspace: RestWorkspace;
161
+ readonly busy?: boolean | undefined;
162
+ /** Called with what changed; nothing is sent when nothing did. */
163
+ readonly onSubmit: (patch: RestWorkspacePatch) => void;
164
+ readonly onCancel?: (() => void) | undefined;
165
+ }
166
+
167
+ /** Where an administrator changes the workspace's settings (ADR-0035). */
168
+ export function WorkspaceSettingsForm(props: WorkspaceSettingsFormProps) {
169
+ const words = useWords();
170
+ const settings = words.settings.workspace;
171
+ const ids = { people: useId(), week: useId(), zone: useId() };
172
+ const current = {
173
+ peopleVisibility: peopleVisibilityOf(props.workspace),
174
+ weekStart: weekStartOf(props.workspace),
175
+ timeZone: timeZoneOf(props.workspace) ?? "",
176
+ };
177
+ const [peopleVisibility, setPeopleVisibility] = useState<PeopleVisibility>(
178
+ current.peopleVisibility,
179
+ );
180
+ const [weekStart, setWeekStart] = useState<WeekStart>(current.weekStart);
181
+ const [timeZone, setTimeZone] = useState(current.timeZone);
182
+ const device = useMemo(deviceTimeZone, []);
183
+ // UTC is offered whatever the browser lists, beside the zone set and the device's.
184
+ const groups = useMemo(
185
+ () => timeZoneGroups(knownTimeZones(), new Date(), ["UTC", current.timeZone, device]),
186
+ [current.timeZone, device],
187
+ );
188
+ const patch: RestWorkspacePatch = {
189
+ ...(peopleVisibility === current.peopleVisibility ? {} : { peopleVisibility }),
190
+ ...(weekStart === current.weekStart ? {} : { weekStart }),
191
+ ...(timeZone === current.timeZone ? {} : { timeZone: timeZone === "" ? null : timeZone }),
192
+ };
193
+ const changed = Object.keys(patch).length > 0;
194
+ const submit = (event: FormEvent) => {
195
+ event.preventDefault();
196
+ if (changed) {
197
+ props.onSubmit(patch);
198
+ }
199
+ };
200
+ return (
201
+ <form className="sc-form sc-settings-form" aria-label={settings.aria} onSubmit={submit}>
202
+ <div className="sc-settings-row">
203
+ <div className="sc-settings-words">
204
+ <label className="sc-settings-label" htmlFor={ids.people}>
205
+ {settings.peopleVisibility}
206
+ </label>
207
+ <p className="sc-settings-hint">{settings.peopleVisibilityHint}</p>
208
+ </div>
209
+ <div className="sc-settings-control">
210
+ <select
211
+ id={ids.people}
212
+ className="sc-input"
213
+ value={peopleVisibility}
214
+ onChange={(event) => setPeopleVisibility(event.target.value as PeopleVisibility)}
215
+ >
216
+ {PEOPLE_VISIBILITIES.map((candidate) => (
217
+ <option key={candidate} value={candidate}>
218
+ {words.vocabulary.peopleVisibility[candidate]}
219
+ </option>
220
+ ))}
221
+ </select>
222
+ </div>
223
+ </div>
224
+ <div className="sc-settings-row">
225
+ <div className="sc-settings-words">
226
+ <label className="sc-settings-label" htmlFor={ids.week}>
227
+ {settings.weekStart}
228
+ </label>
229
+ <p className="sc-settings-hint">{settings.weekStartHint}</p>
230
+ </div>
231
+ <div className="sc-settings-control">
232
+ <select
233
+ id={ids.week}
234
+ className="sc-input"
235
+ value={weekStart}
236
+ onChange={(event) => setWeekStart(event.target.value as WeekStart)}
237
+ >
238
+ {WEEK_STARTS.map((candidate) => (
239
+ <option key={candidate} value={candidate}>
240
+ {words.vocabulary.weekStart[candidate]}
241
+ </option>
242
+ ))}
243
+ </select>
244
+ </div>
245
+ </div>
246
+ <div className="sc-settings-row">
247
+ <div className="sc-settings-words">
248
+ <label className="sc-settings-label" htmlFor={ids.zone}>
249
+ {settings.timeZone}
250
+ </label>
251
+ <p className="sc-settings-hint">{settings.timeZoneHint}</p>
252
+ </div>
253
+ <div className="sc-settings-control">
254
+ <select
255
+ id={ids.zone}
256
+ className="sc-input"
257
+ value={timeZone}
258
+ onChange={(event) => setTimeZone(event.target.value)}
259
+ >
260
+ <option value="">{settings.timeZoneNone}</option>
261
+ {groups.map((group) =>
262
+ group.area === "" ? (
263
+ group.zones.map((zone) => (
264
+ <option key={zone.name} value={zone.name}>
265
+ {labelOf(zone)}
266
+ </option>
267
+ ))
268
+ ) : (
269
+ <optgroup key={group.area} label={group.area}>
270
+ {group.zones.map((zone) => (
271
+ <option key={zone.name} value={zone.name}>
272
+ {labelOf(zone)}
273
+ </option>
274
+ ))}
275
+ </optgroup>
276
+ ),
277
+ )}
278
+ </select>
279
+ {device !== undefined && device !== timeZone && (
280
+ <Button variant="ghost" size="sm" onClick={() => setTimeZone(device)}>
281
+ {settings.timeZoneUseDevice(device)}
282
+ </Button>
283
+ )}
284
+ </div>
285
+ </div>
286
+ <div className="sc-form-actions">
287
+ <Button type="submit" variant="primary" disabled={props.busy === true || !changed}>
288
+ {settings.save}
289
+ </Button>
290
+ {props.onCancel !== undefined && (
291
+ <Button type="button" variant="ghost" onClick={props.onCancel}>
292
+ {words.common.cancel}
293
+ </Button>
294
+ )}
295
+ </div>
296
+ </form>
297
+ );
298
+ }