@skillcdn/console 0.3.2 → 0.4.1
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.
- package/README.md +4 -3
- package/dist/api.d.ts +2 -2
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +2 -2
- package/dist/api.js.map +1 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +132 -18
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +6 -4
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +72 -21
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/pack.d.ts +7 -2
- package/dist/cli/pack.d.ts.map +1 -1
- package/dist/cli/pack.js +16 -2
- package/dist/cli/pack.js.map +1 -1
- package/dist/client.d.ts +19 -3
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +11 -1
- package/dist/client.js.map +1 -1
- package/dist/compat.d.ts +1 -1
- package/dist/compat.js +2 -2
- package/dist/components/board.d.ts +12 -1
- package/dist/components/board.d.ts.map +1 -1
- package/dist/components/board.js +52 -4
- package/dist/components/board.js.map +1 -1
- package/dist/components/calendar.d.ts +4 -0
- package/dist/components/calendar.d.ts.map +1 -1
- package/dist/components/calendar.js +61 -22
- package/dist/components/calendar.js.map +1 -1
- package/dist/components/decision-form.d.ts +12 -4
- package/dist/components/decision-form.d.ts.map +1 -1
- package/dist/components/decision-form.js +24 -6
- package/dist/components/decision-form.js.map +1 -1
- package/dist/components/decision-list.d.ts +4 -0
- package/dist/components/decision-list.d.ts.map +1 -1
- package/dist/components/decision-list.js +9 -6
- package/dist/components/decision-list.js.map +1 -1
- package/dist/components/documents.d.ts.map +1 -1
- package/dist/components/documents.js +3 -3
- package/dist/components/documents.js.map +1 -1
- package/dist/components/event-feed.d.ts.map +1 -1
- package/dist/components/event-feed.js +63 -8
- package/dist/components/event-feed.js.map +1 -1
- package/dist/components/markdown.d.ts.map +1 -1
- package/dist/components/markdown.js +6 -3
- package/dist/components/markdown.js.map +1 -1
- package/dist/components/my-decisions.d.ts +20 -0
- package/dist/components/my-decisions.d.ts.map +1 -0
- package/dist/components/my-decisions.js +36 -0
- package/dist/components/my-decisions.js.map +1 -0
- package/dist/components/pack-connect.d.ts +3 -1
- package/dist/components/pack-connect.d.ts.map +1 -1
- package/dist/components/pack-connect.js +4 -4
- package/dist/components/pack-connect.js.map +1 -1
- package/dist/components/pack-token.d.ts +3 -1
- package/dist/components/pack-token.d.ts.map +1 -1
- package/dist/components/pack-token.js +1 -1
- package/dist/components/pack-token.js.map +1 -1
- package/dist/components/people.d.ts.map +1 -1
- package/dist/components/people.js +14 -3
- package/dist/components/people.js.map +1 -1
- package/dist/components/projects.d.ts.map +1 -1
- package/dist/components/projects.js +3 -3
- package/dist/components/projects.js.map +1 -1
- package/dist/components/remote-pictures.d.ts +6 -0
- package/dist/components/remote-pictures.d.ts.map +1 -0
- package/dist/components/remote-pictures.js +8 -0
- package/dist/components/remote-pictures.js.map +1 -0
- package/dist/components/roadmap.js +1 -1
- package/dist/components/roadmap.js.map +1 -1
- package/dist/components/runs.d.ts.map +1 -1
- package/dist/components/runs.js +1 -1
- package/dist/components/runs.js.map +1 -1
- package/dist/components/shell.d.ts.map +1 -1
- package/dist/components/shell.js +36 -1
- package/dist/components/shell.js.map +1 -1
- package/dist/components/sign-in.d.ts.map +1 -1
- package/dist/components/sign-in.js +49 -2
- package/dist/components/sign-in.js.map +1 -1
- package/dist/components/task-list.js +1 -1
- package/dist/components/task-list.js.map +1 -1
- package/dist/components/task-view.d.ts.map +1 -1
- package/dist/components/task-view.js +2 -2
- package/dist/components/task-view.js.map +1 -1
- package/dist/components/ui.d.ts +13 -2
- package/dist/components/ui.d.ts.map +1 -1
- package/dist/components/ui.js +21 -14
- package/dist/components/ui.js.map +1 -1
- package/dist/console.css +1686 -425
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +83 -13
- package/dist/console.js.map +1 -1
- package/dist/data.d.ts +15 -2
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +18 -19
- package/dist/data.js.map +1 -1
- package/dist/i18n/en.d.ts +48 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +50 -3
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +47 -4
- package/dist/i18n/ko.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/limits.d.ts +14 -0
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +14 -0
- package/dist/limits.js.map +1 -1
- package/dist/router.d.ts +9 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +12 -0
- package/dist/router.js.map +1 -1
- package/dist/routes.d.ts +5 -1
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +5 -1
- package/dist/routes.js.map +1 -1
- package/dist/rules.js +1 -1
- package/dist/rules.js.map +1 -1
- package/dist/schemas.d.ts +272 -4
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +33 -8
- package/dist/schemas.js.map +1 -1
- package/dist/web.d.ts.map +1 -1
- package/dist/web.js +3 -2
- package/dist/web.js.map +1 -1
- package/docs/specs/cli.md +11 -10
- package/docs/specs/rest.md +33 -17
- package/package.json +2 -2
- package/skills/building-a-console/SKILL.md +5 -5
- package/skills/building-a-console/references/the-default-console.md +4 -4
- package/skills/building-a-console/references/the-look-from-a-website.md +5 -5
- package/skills/working-the-board/SKILL.md +6 -4
- package/skills/working-the-board/references/setting-up.md +1 -1
- package/skills/working-the-board/references/terms.md +1 -1
- package/src/api.ts +10 -0
- package/src/cli/cli.ts +162 -19
- package/src/cli/format.ts +11 -4
- package/src/cli/help.ts +79 -20
- package/src/cli/pack.ts +17 -1
- package/src/client.ts +51 -2
- package/src/compat.ts +2 -2
- package/src/components/board.tsx +114 -21
- package/src/components/calendar.tsx +207 -102
- package/src/components/decision-form.tsx +71 -23
- package/src/components/decision-list.tsx +30 -3
- package/src/components/documents.tsx +27 -11
- package/src/components/event-feed.tsx +116 -33
- package/src/components/markdown.tsx +6 -3
- package/src/components/my-decisions.tsx +138 -0
- package/src/components/pack-connect.tsx +10 -7
- package/src/components/pack-token.tsx +4 -1
- package/src/components/people.tsx +20 -8
- package/src/components/projects.tsx +7 -5
- package/src/components/remote-pictures.ts +19 -0
- package/src/components/roadmap.tsx +1 -1
- package/src/components/runs.tsx +5 -1
- package/src/components/shell.tsx +36 -1
- package/src/components/sign-in.tsx +114 -13
- package/src/components/task-list.tsx +1 -1
- package/src/components/task-view.tsx +48 -47
- package/src/components/ui.tsx +70 -14
- package/src/console.tsx +153 -32
- package/src/data.ts +37 -20
- package/src/i18n/en.ts +53 -3
- package/src/i18n/ko.ts +50 -4
- package/src/index.ts +3 -0
- package/src/limits.ts +14 -0
- package/src/router.ts +18 -1
- package/src/routes.ts +5 -1
- package/src/rules.ts +1 -1
- package/src/schemas.ts +37 -8
- package/src/styles/console.css +1686 -425
- package/src/web.ts +3 -2
package/src/components/shell.tsx
CHANGED
|
@@ -186,6 +186,41 @@ function PersonMenu(props: {
|
|
|
186
186
|
|
|
187
187
|
export function Shell(props: ShellProps) {
|
|
188
188
|
const words = useWords();
|
|
189
|
+
// Where the tabs scroll sideways, the current one is brought into the bar's view.
|
|
190
|
+
const tabs = useRef<HTMLElement>(null);
|
|
191
|
+
const current = props.nav.find((item) => item.current)?.href;
|
|
192
|
+
useEffect(() => {
|
|
193
|
+
const bar = tabs.current;
|
|
194
|
+
if (current === undefined || bar === null) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const bring = () => {
|
|
198
|
+
const tab = bar.querySelector<HTMLElement>(".sc-tab-current");
|
|
199
|
+
if (tab === null) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const left = tab.offsetLeft - bar.offsetLeft;
|
|
203
|
+
// The last part of the bar fades where it scrolls (the stylesheet's mask): a tab there
|
|
204
|
+
// counts as out of view.
|
|
205
|
+
const seen = bar.scrollWidth > bar.clientWidth ? bar.clientWidth * 0.85 : bar.clientWidth;
|
|
206
|
+
if (left < bar.scrollLeft || left + tab.offsetWidth > bar.scrollLeft + seen) {
|
|
207
|
+
bar.scrollLeft = left - (bar.clientWidth - tab.offsetWidth) / 2;
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
bring();
|
|
211
|
+
// The tabs grow once the page's face has loaded, and the bar narrows with the window: either
|
|
212
|
+
// may put the current tab out of view again.
|
|
213
|
+
if (typeof ResizeObserver === "undefined") {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const watch = new ResizeObserver(bring);
|
|
217
|
+
watch.observe(bar);
|
|
218
|
+
const inner = bar.firstElementChild;
|
|
219
|
+
if (inner !== null) {
|
|
220
|
+
watch.observe(inner);
|
|
221
|
+
}
|
|
222
|
+
return () => watch.disconnect();
|
|
223
|
+
}, [current]);
|
|
189
224
|
const follow = (href: string) => (event: React.MouseEvent) => {
|
|
190
225
|
if (isPlainClick(event)) {
|
|
191
226
|
event.preventDefault();
|
|
@@ -259,7 +294,7 @@ export function Shell(props: ShellProps) {
|
|
|
259
294
|
</div>
|
|
260
295
|
</div>
|
|
261
296
|
{props.nav.length > 0 && (
|
|
262
|
-
<nav className="sc-tabs" aria-label={words.shell.pages}>
|
|
297
|
+
<nav ref={tabs} className="sc-tabs" aria-label={words.shell.pages}>
|
|
263
298
|
<div className="sc-tabs-inner">
|
|
264
299
|
{props.nav.map((item) => (
|
|
265
300
|
<a
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type MouseEvent, useEffect, useState } from "react";
|
|
2
|
+
import { loginPath, type ProviderKey, type RestProvider, type SignInFailure } from "../api.js";
|
|
2
3
|
import { useWords } from "../i18n/index.js";
|
|
3
4
|
import type { Brand } from "./shell.js";
|
|
4
|
-
import { Callout } from "./ui.js";
|
|
5
|
+
import { Callout, cx } from "./ui.js";
|
|
5
6
|
|
|
6
7
|
// The page a person sees before they are anyone: the way to each provider the deployment has,
|
|
7
|
-
// and what went wrong the last time, when something did.
|
|
8
|
+
// who gets in through it, and what went wrong the last time, when something did.
|
|
8
9
|
|
|
9
10
|
export interface SignInProps {
|
|
10
11
|
readonly title: string;
|
|
@@ -19,6 +20,35 @@ export interface SignInProps {
|
|
|
19
20
|
|
|
20
21
|
export function SignIn(props: SignInProps) {
|
|
21
22
|
const words = useWords().signIn;
|
|
23
|
+
// The way being taken: its button says so from the click until the page leaves, and says so
|
|
24
|
+
// no more when the browser brings this page back from its cache, as on the way back.
|
|
25
|
+
const [taken, setTaken] = useState<ProviderKey | undefined>(undefined);
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const shown = (event: PageTransitionEvent): void => {
|
|
28
|
+
if (event.persisted) {
|
|
29
|
+
setTaken(undefined);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
window.addEventListener("pageshow", shown);
|
|
33
|
+
return () => window.removeEventListener("pageshow", shown);
|
|
34
|
+
}, []);
|
|
35
|
+
const take = (provider: ProviderKey) => (event: MouseEvent<HTMLAnchorElement>) => {
|
|
36
|
+
if (taken !== undefined) {
|
|
37
|
+
// One way at a time: a second click, on any of the buttons, goes nowhere.
|
|
38
|
+
event.preventDefault();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
setTaken(provider);
|
|
42
|
+
};
|
|
43
|
+
const admitsOf = (admits: NonNullable<RestProvider["admits"]>): string => {
|
|
44
|
+
if (admits.organization !== null) {
|
|
45
|
+
return words.admits.organization(admits.organization);
|
|
46
|
+
}
|
|
47
|
+
if (admits.domains.length > 0) {
|
|
48
|
+
return words.admits.domains(admits.domains.join(", "));
|
|
49
|
+
}
|
|
50
|
+
return words.admits.listed;
|
|
51
|
+
};
|
|
22
52
|
return (
|
|
23
53
|
<div className="sc-sign-in">
|
|
24
54
|
{props.brand?.symbol !== undefined && (
|
|
@@ -44,18 +74,89 @@ export function SignIn(props: SignInProps) {
|
|
|
44
74
|
</Callout>
|
|
45
75
|
) : (
|
|
46
76
|
<div className="sc-sign-in-ways">
|
|
47
|
-
{props.providers.map((provider) =>
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
key={provider.key}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
77
|
+
{props.providers.map((provider) => {
|
|
78
|
+
const going = taken === provider.key;
|
|
79
|
+
return (
|
|
80
|
+
<div key={provider.key} className="sc-sign-in-way">
|
|
81
|
+
{/* A real navigation: signing in happens at the provider, not in this page. */}
|
|
82
|
+
<a
|
|
83
|
+
className={cx(
|
|
84
|
+
"sc-button sc-button-primary sc-sign-in-button",
|
|
85
|
+
going && "sc-sign-in-button-going",
|
|
86
|
+
)}
|
|
87
|
+
href={loginPath(provider.key, props.returnTo)}
|
|
88
|
+
aria-busy={going ? true : undefined}
|
|
89
|
+
aria-disabled={taken !== undefined && !going ? true : undefined}
|
|
90
|
+
onClick={take(provider.key)}
|
|
91
|
+
>
|
|
92
|
+
<ProviderMark provider={provider.key} />
|
|
93
|
+
<span className="sc-sign-in-button-label">
|
|
94
|
+
{going ? words.going(provider.label) : words.signInWith(provider.label)}
|
|
95
|
+
</span>
|
|
96
|
+
{going && (
|
|
97
|
+
<span className="sc-spinner sc-sign-in-spinner" aria-hidden="true">
|
|
98
|
+
<span className="sc-spinner-ring" />
|
|
99
|
+
</span>
|
|
100
|
+
)}
|
|
101
|
+
</a>
|
|
102
|
+
{provider.admits !== undefined && (
|
|
103
|
+
<p className="sc-sign-in-admits">{admitsOf(provider.admits)}</p>
|
|
104
|
+
)}
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
})}
|
|
57
108
|
</div>
|
|
58
109
|
)}
|
|
59
110
|
</div>
|
|
60
111
|
);
|
|
61
112
|
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The provider's mark, as people know it from sign-in buttons elsewhere, beside the label and
|
|
116
|
+
* decorative: the host's own mark in the button's text color, Google's lettermark on a light
|
|
117
|
+
* tile, since its colors are its own. A provider without a mark shows the label alone.
|
|
118
|
+
*/
|
|
119
|
+
function ProviderMark(props: { readonly provider: ProviderKey }) {
|
|
120
|
+
if (props.provider === "gh") {
|
|
121
|
+
return (
|
|
122
|
+
<svg
|
|
123
|
+
className="sc-sign-in-mark"
|
|
124
|
+
viewBox="0 0 16 16"
|
|
125
|
+
width="20"
|
|
126
|
+
height="20"
|
|
127
|
+
aria-hidden="true"
|
|
128
|
+
focusable="false"
|
|
129
|
+
>
|
|
130
|
+
<path
|
|
131
|
+
fill="currentColor"
|
|
132
|
+
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"
|
|
133
|
+
/>
|
|
134
|
+
</svg>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
if (props.provider === "google") {
|
|
138
|
+
return (
|
|
139
|
+
<span className="sc-sign-in-mark sc-sign-in-mark-tile" aria-hidden="true">
|
|
140
|
+
<svg viewBox="0 0 48 48" width="16" height="16" aria-hidden="true" focusable="false">
|
|
141
|
+
<path
|
|
142
|
+
fill="#FFC107"
|
|
143
|
+
d="M43.611 20.083H42V20H24v8h11.303c-1.649 4.657-6.08 8-11.303 8-6.627 0-12-5.373-12-12s5.373-12 12-12c3.059 0 5.842 1.154 7.961 3.039l5.657-5.657C34.046 6.053 29.268 4 24 4 12.955 4 4 12.955 4 24s8.955 20 20 20 20-8.955 20-20c0-1.341-.138-2.65-.389-3.917z"
|
|
144
|
+
/>
|
|
145
|
+
<path
|
|
146
|
+
fill="#FF3D00"
|
|
147
|
+
d="M6.306 14.691l6.571 4.819C14.655 15.108 18.961 12 24 12c3.059 0 5.842 1.154 7.961 3.039l5.657-5.657C34.046 6.053 29.268 4 24 4 16.318 4 9.656 8.337 6.306 14.691z"
|
|
148
|
+
/>
|
|
149
|
+
<path
|
|
150
|
+
fill="#4CAF50"
|
|
151
|
+
d="M24 44c5.166 0 9.86-1.977 13.409-5.192l-6.19-5.238A11.91 11.91 0 0 1 24 36c-5.202 0-9.619-3.317-11.283-7.946l-6.522 5.025C9.505 39.556 16.227 44 24 44z"
|
|
152
|
+
/>
|
|
153
|
+
<path
|
|
154
|
+
fill="#1976D2"
|
|
155
|
+
d="M43.611 20.083H42V20H24v8h11.303a12.04 12.04 0 0 1-4.087 5.571l.003-.002 6.19 5.238C36.971 39.205 44 34 44 24c0-1.341-.138-2.65-.389-3.917z"
|
|
156
|
+
/>
|
|
157
|
+
</svg>
|
|
158
|
+
</span>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
@@ -45,7 +45,7 @@ export function TaskList(props: TaskListProps) {
|
|
|
45
45
|
{task.category !== null && <CategoryBadge category={task.category} />}
|
|
46
46
|
{props.showState === true && <StateBadge state={task.state} />}
|
|
47
47
|
{task.openDecisions > 0 && (
|
|
48
|
-
<Badge tone="warning" title={words.board.decisionsTitle}>
|
|
48
|
+
<Badge tone="warning" kind="count" title={words.board.decisionsTitle}>
|
|
49
49
|
{words.board.decisions(task.openDecisions)}
|
|
50
50
|
</Badge>
|
|
51
51
|
)}
|
|
@@ -142,26 +142,56 @@ export function TaskView(props: TaskViewProps) {
|
|
|
142
142
|
return (
|
|
143
143
|
<div className="sc-task">
|
|
144
144
|
<header className="sc-task-header">
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
<h1 className="sc-task-title">
|
|
154
|
-
<span className="sc-card-number">#{task.number}</span> {task.title}
|
|
155
|
-
</h1>
|
|
156
|
-
<div className="sc-task-badges">
|
|
157
|
-
<StateBadge state={task.state} />
|
|
158
|
-
{task.archivedAt !== null && <Badge tone="neutral">{words.task.archived}</Badge>}
|
|
159
|
-
{task.category !== null && <CategoryBadge category={task.category} />}
|
|
160
|
-
<PriorityBadge priority={task.priority} />
|
|
161
|
-
{task.dueOn !== null && (
|
|
162
|
-
<DueBadge dueOn={task.dueOn} state={task.state} today={props.today} />
|
|
145
|
+
<div className="sc-task-heading">
|
|
146
|
+
{parent !== undefined && (
|
|
147
|
+
<p className="sc-task-parent">
|
|
148
|
+
{words.task.partOf}{" "}
|
|
149
|
+
<a href={props.taskHref(parent)}>
|
|
150
|
+
#{parent.number} {parent.title}
|
|
151
|
+
</a>
|
|
152
|
+
</p>
|
|
163
153
|
)}
|
|
154
|
+
<h1 className="sc-task-title">
|
|
155
|
+
<span className="sc-card-number">#{task.number}</span> {task.title}
|
|
156
|
+
</h1>
|
|
157
|
+
<div className="sc-task-badges">
|
|
158
|
+
<StateBadge state={task.state} />
|
|
159
|
+
{task.archivedAt !== null && <Badge tone="neutral">{words.task.archived}</Badge>}
|
|
160
|
+
{task.category !== null && <CategoryBadge category={task.category} />}
|
|
161
|
+
<PriorityBadge priority={task.priority} />
|
|
162
|
+
{task.dueOn !== null && (
|
|
163
|
+
<DueBadge dueOn={task.dueOn} state={task.state} today={props.today} />
|
|
164
|
+
)}
|
|
165
|
+
</div>
|
|
164
166
|
</div>
|
|
167
|
+
{props.formHref !== undefined && (
|
|
168
|
+
<div className="sc-task-actions">
|
|
169
|
+
<a className="sc-button sc-button-secondary" href={props.formHref("edit")}>
|
|
170
|
+
{words.common.edit}
|
|
171
|
+
</a>
|
|
172
|
+
<a className="sc-button sc-button-secondary" href={props.formHref("ask")}>
|
|
173
|
+
{words.task.raiseDecision}
|
|
174
|
+
</a>
|
|
175
|
+
{props.onArchive !== undefined && task.archivedAt === null && (
|
|
176
|
+
<Button
|
|
177
|
+
variant="secondary"
|
|
178
|
+
disabled={props.busy === true}
|
|
179
|
+
onClick={() => props.onArchive?.(task)}
|
|
180
|
+
>
|
|
181
|
+
{words.task.archive}
|
|
182
|
+
</Button>
|
|
183
|
+
)}
|
|
184
|
+
{props.onRestore !== undefined && task.archivedAt !== null && (
|
|
185
|
+
<Button
|
|
186
|
+
variant="secondary"
|
|
187
|
+
disabled={props.busy === true}
|
|
188
|
+
onClick={() => props.onRestore?.(task)}
|
|
189
|
+
>
|
|
190
|
+
{words.task.restore}
|
|
191
|
+
</Button>
|
|
192
|
+
)}
|
|
193
|
+
</div>
|
|
194
|
+
)}
|
|
165
195
|
</header>
|
|
166
196
|
<div className="sc-task-body">
|
|
167
197
|
<div className="sc-task-main">
|
|
@@ -193,7 +223,6 @@ export function TaskView(props: TaskViewProps) {
|
|
|
193
223
|
<section className="sc-task-section" aria-label={words.task.raiseDecision}>
|
|
194
224
|
<h2 className="sc-section-title">{words.task.raiseDecision}</h2>
|
|
195
225
|
<DecisionForm
|
|
196
|
-
tasks={props.tasks}
|
|
197
226
|
taskId={task.id}
|
|
198
227
|
busy={props.busy}
|
|
199
228
|
onSubmit={props.onRaiseDecision}
|
|
@@ -360,34 +389,6 @@ export function TaskView(props: TaskViewProps) {
|
|
|
360
389
|
</dd>
|
|
361
390
|
</div>
|
|
362
391
|
</dl>
|
|
363
|
-
{props.formHref !== undefined && (
|
|
364
|
-
<div className="sc-task-actions">
|
|
365
|
-
<a className="sc-button sc-button-secondary" href={props.formHref("edit")}>
|
|
366
|
-
{words.common.edit}
|
|
367
|
-
</a>
|
|
368
|
-
<a className="sc-button sc-button-secondary" href={props.formHref("ask")}>
|
|
369
|
-
{words.task.raiseDecision}
|
|
370
|
-
</a>
|
|
371
|
-
{props.onArchive !== undefined && task.archivedAt === null && (
|
|
372
|
-
<Button
|
|
373
|
-
variant="secondary"
|
|
374
|
-
disabled={props.busy === true}
|
|
375
|
-
onClick={() => props.onArchive?.(task)}
|
|
376
|
-
>
|
|
377
|
-
{words.task.archive}
|
|
378
|
-
</Button>
|
|
379
|
-
)}
|
|
380
|
-
{props.onRestore !== undefined && task.archivedAt !== null && (
|
|
381
|
-
<Button
|
|
382
|
-
variant="secondary"
|
|
383
|
-
disabled={props.busy === true}
|
|
384
|
-
onClick={() => props.onRestore?.(task)}
|
|
385
|
-
>
|
|
386
|
-
{words.task.restore}
|
|
387
|
-
</Button>
|
|
388
|
-
)}
|
|
389
|
-
</div>
|
|
390
|
-
)}
|
|
391
392
|
</aside>
|
|
392
393
|
</div>
|
|
393
394
|
</div>
|
package/src/components/ui.tsx
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from "../api.js";
|
|
12
12
|
import { dayOf, daysBetween, formatDay, today } from "../dates.js";
|
|
13
13
|
import { useLanguage, useWords } from "../i18n/index.js";
|
|
14
|
+
import { useRemotePictureSrc } from "./remote-pictures.js";
|
|
14
15
|
|
|
15
16
|
// Small building blocks. Each one is a class in styles/console.css and nothing more. Classes
|
|
16
17
|
// are prefixed `sc-` so that a custom console's own styles never collide with them. The words
|
|
@@ -43,11 +44,23 @@ export function Button(
|
|
|
43
44
|
|
|
44
45
|
export function Badge(props: {
|
|
45
46
|
readonly tone?: "neutral" | "accent" | "point" | "success" | "warning" | "danger";
|
|
47
|
+
/** What the badge says, which the stylesheet shows in its shape: a state carries a dot, a priority is outlined, a due day is a square-cornered tag, a count is figures. */
|
|
48
|
+
readonly kind?: "state" | "priority" | "due" | "count" | undefined;
|
|
49
|
+
/** A class of the badge's own beside the tone's and the kind's, for a step within a kind. */
|
|
50
|
+
readonly className?: string | undefined;
|
|
46
51
|
readonly title?: string;
|
|
47
52
|
readonly children: ReactNode;
|
|
48
53
|
}) {
|
|
49
54
|
return (
|
|
50
|
-
<span
|
|
55
|
+
<span
|
|
56
|
+
className={cx(
|
|
57
|
+
"sc-badge",
|
|
58
|
+
`sc-badge-${props.tone ?? "neutral"}`,
|
|
59
|
+
props.kind !== undefined && `sc-badge-kind-${props.kind}`,
|
|
60
|
+
props.className,
|
|
61
|
+
)}
|
|
62
|
+
title={props.title}
|
|
63
|
+
>
|
|
51
64
|
{props.children}
|
|
52
65
|
</span>
|
|
53
66
|
);
|
|
@@ -84,13 +97,15 @@ export function personName(person: {
|
|
|
84
97
|
}
|
|
85
98
|
|
|
86
99
|
/**
|
|
87
|
-
* A person's picture as the git host serves it
|
|
88
|
-
* a whole when there is none or it cannot be loaded,
|
|
100
|
+
* A person's picture as the git host serves it, read through the console (ADR-0048): loaded
|
|
101
|
+
* lazily, without a referrer, and gone as a whole when there is none or it cannot be loaded,
|
|
102
|
+
* leaving the initial of the name instead.
|
|
89
103
|
*/
|
|
90
104
|
export function Avatar(props: {
|
|
91
105
|
readonly person: RestPerson;
|
|
92
106
|
readonly size?: "xs" | "sm" | "md" | "lg";
|
|
93
107
|
}) {
|
|
108
|
+
const remote = useRemotePictureSrc();
|
|
94
109
|
const size = props.size ?? "md";
|
|
95
110
|
const initial = personName(props.person).slice(0, 1).toUpperCase();
|
|
96
111
|
if (props.person.avatar === null) {
|
|
@@ -106,7 +121,7 @@ export function Avatar(props: {
|
|
|
106
121
|
return (
|
|
107
122
|
<img
|
|
108
123
|
className={cx("sc-avatar", `sc-avatar-${size}`)}
|
|
109
|
-
src={props.person.avatar}
|
|
124
|
+
src={remote(props.person.avatar)}
|
|
110
125
|
alt=""
|
|
111
126
|
loading="lazy"
|
|
112
127
|
decoding="async"
|
|
@@ -195,7 +210,7 @@ export const ROLE_LABELS: Readonly<Record<PersonRole, string>> = {
|
|
|
195
210
|
export function RoleBadge(props: { readonly role: PersonRole }) {
|
|
196
211
|
const words = useWords();
|
|
197
212
|
return (
|
|
198
|
-
<Badge tone={props.role === "admin" ? "
|
|
213
|
+
<Badge tone={props.role === "admin" ? "accent" : "neutral"}>
|
|
199
214
|
{words.vocabulary.role[props.role]}
|
|
200
215
|
</Badge>
|
|
201
216
|
);
|
|
@@ -211,7 +226,11 @@ export function StateBadge(props: { readonly state: TaskState }) {
|
|
|
211
226
|
: props.state === "in_progress" || props.state === "in_review"
|
|
212
227
|
? "accent"
|
|
213
228
|
: "neutral";
|
|
214
|
-
return
|
|
229
|
+
return (
|
|
230
|
+
<Badge tone={tone} kind="state">
|
|
231
|
+
{words.vocabulary.state[props.state]}
|
|
232
|
+
</Badge>
|
|
233
|
+
);
|
|
215
234
|
}
|
|
216
235
|
|
|
217
236
|
export function PriorityBadge(props: { readonly priority: TaskPriority }) {
|
|
@@ -221,7 +240,11 @@ export function PriorityBadge(props: { readonly priority: TaskPriority }) {
|
|
|
221
240
|
}
|
|
222
241
|
const tone =
|
|
223
242
|
props.priority === "urgent" ? "danger" : props.priority === "high" ? "point" : "neutral";
|
|
224
|
-
return
|
|
243
|
+
return (
|
|
244
|
+
<Badge tone={tone} kind="priority">
|
|
245
|
+
{words.vocabulary.priority[props.priority]}
|
|
246
|
+
</Badge>
|
|
247
|
+
);
|
|
225
248
|
}
|
|
226
249
|
|
|
227
250
|
/** A category as a badge, in its colour: a class the stylesheet paints, never a value typed by hand. */
|
|
@@ -245,7 +268,11 @@ export function GoalStateBadge(props: { readonly state: GoalState }) {
|
|
|
245
268
|
: props.state === "dropped"
|
|
246
269
|
? "neutral"
|
|
247
270
|
: "neutral";
|
|
248
|
-
return
|
|
271
|
+
return (
|
|
272
|
+
<Badge tone={tone} kind="state">
|
|
273
|
+
{words.vocabulary.goalState[props.state]}
|
|
274
|
+
</Badge>
|
|
275
|
+
);
|
|
249
276
|
}
|
|
250
277
|
|
|
251
278
|
/**
|
|
@@ -266,29 +293,45 @@ export function DueBadge(props: {
|
|
|
266
293
|
const days = daysBetween(now, props.dueOn);
|
|
267
294
|
const day = formatDay(props.dueOn, tag);
|
|
268
295
|
if (!open) {
|
|
269
|
-
return
|
|
296
|
+
return (
|
|
297
|
+
<Badge tone="neutral" kind="due">
|
|
298
|
+
{words.board.due.on(day)}
|
|
299
|
+
</Badge>
|
|
300
|
+
);
|
|
270
301
|
}
|
|
271
302
|
if (days < 0) {
|
|
272
303
|
return (
|
|
273
|
-
<Badge tone="danger" title={day}>
|
|
304
|
+
<Badge tone="danger" kind="due" title={day}>
|
|
274
305
|
{words.board.due.overdue(-days)}
|
|
275
306
|
</Badge>
|
|
276
307
|
);
|
|
277
308
|
}
|
|
278
309
|
if (days === 0) {
|
|
279
|
-
return
|
|
310
|
+
return (
|
|
311
|
+
<Badge tone="point" kind="due" className="sc-badge-due-today">
|
|
312
|
+
{words.board.due.today}
|
|
313
|
+
</Badge>
|
|
314
|
+
);
|
|
280
315
|
}
|
|
281
316
|
if (days === 1) {
|
|
282
|
-
return
|
|
317
|
+
return (
|
|
318
|
+
<Badge tone="point" kind="due">
|
|
319
|
+
{words.board.due.tomorrow}
|
|
320
|
+
</Badge>
|
|
321
|
+
);
|
|
283
322
|
}
|
|
284
323
|
if (days <= 7) {
|
|
285
324
|
return (
|
|
286
|
-
<Badge tone="
|
|
325
|
+
<Badge tone="warning" kind="due" title={day}>
|
|
287
326
|
{words.board.due.inDays(days)}
|
|
288
327
|
</Badge>
|
|
289
328
|
);
|
|
290
329
|
}
|
|
291
|
-
return
|
|
330
|
+
return (
|
|
331
|
+
<Badge tone="neutral" kind="due">
|
|
332
|
+
{words.board.due.on(day)}
|
|
333
|
+
</Badge>
|
|
334
|
+
);
|
|
292
335
|
}
|
|
293
336
|
|
|
294
337
|
/** How far a goal is, read from its tasks: the done ones, the ones at work, and the rest. */
|
|
@@ -402,3 +445,16 @@ export function Time(props: {
|
|
|
402
445
|
</time>
|
|
403
446
|
);
|
|
404
447
|
}
|
|
448
|
+
|
|
449
|
+
/** A link that reads as a button: where a form is, for instance. */
|
|
450
|
+
export function LinkButton(props: {
|
|
451
|
+
readonly href: string;
|
|
452
|
+
readonly variant?: "primary" | "secondary";
|
|
453
|
+
readonly children: ReactNode;
|
|
454
|
+
}) {
|
|
455
|
+
return (
|
|
456
|
+
<a className={`sc-button sc-button-${props.variant ?? "secondary"}`} href={props.href}>
|
|
457
|
+
{props.children}
|
|
458
|
+
</a>
|
|
459
|
+
);
|
|
460
|
+
}
|