@skillcdn/console 0.1.17 → 0.1.19
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 -4
- package/SKILLCDN.md +1 -1
- package/dist/api.d.ts +3 -2
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +2 -1
- package/dist/api.js.map +1 -1
- package/dist/cli/cli.d.ts +11 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +496 -51
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/credentials.d.ts +102 -3
- package/dist/cli/credentials.d.ts.map +1 -1
- package/dist/cli/credentials.js +177 -8
- package/dist/cli/credentials.js.map +1 -1
- package/dist/cli/directory.d.ts +6 -0
- package/dist/cli/directory.d.ts.map +1 -1
- package/dist/cli/directory.js +37 -6
- package/dist/cli/directory.js.map +1 -1
- package/dist/cli/format.d.ts +11 -2
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +114 -5
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +49 -25
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/install.d.ts +21 -0
- package/dist/cli/install.d.ts.map +1 -0
- package/dist/cli/install.js +87 -0
- package/dist/cli/install.js.map +1 -0
- package/dist/cli/main.js +22 -3
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/notices.d.ts +13 -0
- package/dist/cli/notices.d.ts.map +1 -0
- package/dist/cli/notices.js +31 -0
- package/dist/cli/notices.js.map +1 -0
- package/dist/client.d.ts +36 -3
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +22 -4
- package/dist/client.js.map +1 -1
- package/dist/compat.d.ts +66 -0
- package/dist/compat.d.ts.map +1 -0
- package/dist/compat.js +115 -0
- package/dist/compat.js.map +1 -0
- package/dist/components/board.d.ts +9 -1
- package/dist/components/board.d.ts.map +1 -1
- package/dist/components/board.js +82 -5
- package/dist/components/board.js.map +1 -1
- package/dist/components/calendar.d.ts +3 -1
- package/dist/components/calendar.d.ts.map +1 -1
- package/dist/components/calendar.js +21 -2
- package/dist/components/calendar.js.map +1 -1
- package/dist/components/comments.d.ts.map +1 -1
- package/dist/components/comments.js +12 -4
- package/dist/components/comments.js.map +1 -1
- package/dist/components/event-feed.d.ts.map +1 -1
- package/dist/components/event-feed.js +4 -0
- package/dist/components/event-feed.js.map +1 -1
- package/dist/components/goals.d.ts +20 -2
- package/dist/components/goals.d.ts.map +1 -1
- package/dist/components/goals.js +50 -13
- package/dist/components/goals.js.map +1 -1
- package/dist/components/markdown.d.ts +6 -0
- package/dist/components/markdown.d.ts.map +1 -1
- package/dist/components/markdown.js +7 -0
- package/dist/components/markdown.js.map +1 -1
- package/dist/components/pictures.d.ts +68 -0
- package/dist/components/pictures.d.ts.map +1 -0
- package/dist/components/pictures.js +213 -0
- package/dist/components/pictures.js.map +1 -0
- package/dist/components/projects.d.ts +12 -0
- package/dist/components/projects.d.ts.map +1 -1
- package/dist/components/projects.js +15 -0
- package/dist/components/projects.js.map +1 -1
- package/dist/components/roadmap.d.ts +37 -0
- package/dist/components/roadmap.d.ts.map +1 -0
- package/dist/components/roadmap.js +76 -0
- package/dist/components/roadmap.js.map +1 -0
- package/dist/components/task-view.d.ts +3 -0
- package/dist/components/task-view.d.ts.map +1 -1
- package/dist/components/task-view.js +6 -4
- package/dist/components/task-view.js.map +1 -1
- package/dist/components/tokens.d.ts +2 -0
- package/dist/components/tokens.d.ts.map +1 -1
- package/dist/components/tokens.js +13 -3
- package/dist/components/tokens.js.map +1 -1
- package/dist/console.css +486 -3
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +132 -17
- package/dist/console.js.map +1 -1
- package/dist/data.d.ts +16 -1
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +48 -3
- package/dist/data.js.map +1 -1
- package/dist/i18n/en.d.ts +76 -0
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +74 -2
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +74 -2
- package/dist/i18n/ko.js.map +1 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -4
- package/dist/index.js.map +1 -1
- package/dist/router.d.ts +31 -7
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +48 -8
- package/dist/router.js.map +1 -1
- package/dist/rules.js +5 -5
- package/dist/rules.js.map +1 -1
- package/dist/schemas.d.ts +200 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +96 -0
- package/dist/schemas.js.map +1 -1
- package/dist/vocabulary.d.ts +2 -2
- package/dist/vocabulary.d.ts.map +1 -1
- package/dist/vocabulary.js +3 -0
- package/dist/vocabulary.js.map +1 -1
- package/docs/specs/cli.md +26 -15
- package/docs/specs/rest.md +31 -9
- package/package.json +3 -2
- package/skills/working-the-board/SKILL.md +11 -4
- package/src/api.ts +36 -0
- package/src/cli/cli.ts +616 -54
- package/src/cli/credentials.ts +276 -14
- package/src/cli/directory.ts +43 -6
- package/src/cli/format.ts +146 -5
- package/src/cli/help.ts +51 -25
- package/src/cli/install.ts +111 -0
- package/src/cli/main.ts +23 -3
- package/src/cli/notices.ts +48 -0
- package/src/client.ts +91 -6
- package/src/compat.ts +173 -0
- package/src/components/board.tsx +154 -8
- package/src/components/calendar.tsx +41 -1
- package/src/components/comments.tsx +20 -10
- package/src/components/event-feed.tsx +4 -0
- package/src/components/goals.tsx +234 -88
- package/src/components/markdown.tsx +19 -0
- package/src/components/pictures.tsx +389 -0
- package/src/components/projects.tsx +68 -0
- package/src/components/roadmap.tsx +242 -0
- package/src/components/task-view.tsx +43 -2
- package/src/components/tokens.tsx +38 -1
- package/src/console.tsx +278 -4
- package/src/data.ts +73 -2
- package/src/i18n/en.ts +77 -2
- package/src/i18n/ko.ts +77 -2
- package/src/index.ts +35 -0
- package/src/router.ts +70 -11
- package/src/rules.ts +5 -5
- package/src/schemas.ts +117 -0
- package/src/styles/console.css +486 -3
- package/src/vocabulary.ts +3 -0
package/src/components/goals.tsx
CHANGED
|
@@ -4,20 +4,35 @@ import {
|
|
|
4
4
|
type GoalState,
|
|
5
5
|
MAX_BODY_LENGTH,
|
|
6
6
|
MAX_TITLE_LENGTH,
|
|
7
|
+
type RestCategory,
|
|
7
8
|
type RestGoal,
|
|
8
9
|
type RestGoalInput,
|
|
9
10
|
type RestTask,
|
|
10
11
|
TASK_STATES,
|
|
11
12
|
} from "../api.js";
|
|
12
|
-
import {
|
|
13
|
+
import { formatDays, monthOf } from "../dates.js";
|
|
14
|
+
import { useLanguage, useWords } from "../i18n/index.js";
|
|
15
|
+
import { GOAL_TASK_VIEWS, type GoalTaskView } from "../router.js";
|
|
13
16
|
import { Markdown } from "./markdown.js";
|
|
14
17
|
import { TaskList } from "./task-list.js";
|
|
15
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
Button,
|
|
20
|
+
CategoryBadge,
|
|
21
|
+
cx,
|
|
22
|
+
Day,
|
|
23
|
+
DueBadge,
|
|
24
|
+
GoalStateBadge,
|
|
25
|
+
PersonChip,
|
|
26
|
+
ProgressBar,
|
|
27
|
+
Time,
|
|
28
|
+
} from "./ui.js";
|
|
16
29
|
|
|
17
|
-
// The roadmap of a project (ADR-0019): its goals, each
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
// the
|
|
30
|
+
// The roadmap of a project (ADR-0019): its goals, each planned in days as a task is, a start
|
|
31
|
+
// and a target, sorted into a category of the workspace (ADR-0026), with how far its tasks
|
|
32
|
+
// are, read from the board as it stands. The list, one goal with its tasks (as the board of
|
|
33
|
+
// them, the list by state, or the calendar, the first two given by the composition from the
|
|
34
|
+
// board's own parts), and the form an owner sets one with. Each takes its data as props and
|
|
35
|
+
// nothing from the network; what people wrote is shown as text, never as HTML.
|
|
21
36
|
|
|
22
37
|
/** A goal's day on the calendar's terms: reached or let go, it is a plain fact; else it may be late. */
|
|
23
38
|
const dueState = (goal: RestGoal) =>
|
|
@@ -33,6 +48,7 @@ export interface GoalListProps {
|
|
|
33
48
|
|
|
34
49
|
export function GoalList(props: GoalListProps) {
|
|
35
50
|
const words = useWords().roadmap;
|
|
51
|
+
const { tag } = useLanguage();
|
|
36
52
|
if (props.goals.length === 0) {
|
|
37
53
|
return <>{props.empty ?? null}</>;
|
|
38
54
|
}
|
|
@@ -44,7 +60,11 @@ export function GoalList(props: GoalListProps) {
|
|
|
44
60
|
<a className="sc-goal-title" href={props.goalHref(goal)}>
|
|
45
61
|
{goal.title}
|
|
46
62
|
</a>
|
|
63
|
+
{goal.category !== null && <CategoryBadge category={goal.category} />}
|
|
47
64
|
<GoalStateBadge state={goal.state} />
|
|
65
|
+
{goal.startsOn !== null && goal.dueOn !== null && goal.startsOn < goal.dueOn && (
|
|
66
|
+
<span className="sc-goal-days">{formatDays(goal.startsOn, goal.dueOn, tag)}</span>
|
|
67
|
+
)}
|
|
48
68
|
{goal.dueOn !== null && (
|
|
49
69
|
<DueBadge dueOn={goal.dueOn} state={dueState(goal)} today={props.today} />
|
|
50
70
|
)}
|
|
@@ -59,6 +79,8 @@ export function GoalList(props: GoalListProps) {
|
|
|
59
79
|
export interface GoalFormProps {
|
|
60
80
|
/** The goal to change; left out, the form sets a new one. */
|
|
61
81
|
readonly goal?: RestGoal | undefined;
|
|
82
|
+
/** The workspace's categories to sort the goal into; left out, the form offers none. */
|
|
83
|
+
readonly categories?: readonly RestCategory[] | undefined;
|
|
62
84
|
readonly busy?: boolean | undefined;
|
|
63
85
|
readonly error?: string | undefined;
|
|
64
86
|
readonly onSubmit: (input: RestGoalInput) => void;
|
|
@@ -72,11 +94,20 @@ export function GoalForm(props: GoalFormProps) {
|
|
|
72
94
|
const [title, setTitle] = useState(goal?.title ?? "");
|
|
73
95
|
const [body, setBody] = useState(goal?.body ?? "");
|
|
74
96
|
const [state, setState] = useState<GoalState>(goal?.state ?? "planned");
|
|
97
|
+
const [category, setCategory] = useState(goal?.category?.key ?? "");
|
|
98
|
+
const [startsOn, setStartsOn] = useState(goal?.startsOn ?? "");
|
|
75
99
|
const [dueOn, setDueOn] = useState(goal?.dueOn ?? "");
|
|
76
100
|
|
|
77
101
|
const submit = (event: FormEvent) => {
|
|
78
102
|
event.preventDefault();
|
|
79
|
-
props.onSubmit({
|
|
103
|
+
props.onSubmit({
|
|
104
|
+
title: title.trim(),
|
|
105
|
+
body,
|
|
106
|
+
state,
|
|
107
|
+
category: category === "" ? null : category,
|
|
108
|
+
startsOn: startsOn === "" ? null : startsOn,
|
|
109
|
+
dueOn: dueOn === "" ? null : dueOn,
|
|
110
|
+
});
|
|
80
111
|
};
|
|
81
112
|
|
|
82
113
|
return (
|
|
@@ -117,12 +148,42 @@ export function GoalForm(props: GoalFormProps) {
|
|
|
117
148
|
))}
|
|
118
149
|
</select>
|
|
119
150
|
</label>
|
|
151
|
+
{props.categories !== undefined && (
|
|
152
|
+
<label className="sc-field">
|
|
153
|
+
<span className="sc-field-label">{form.category}</span>
|
|
154
|
+
<select
|
|
155
|
+
className="sc-input"
|
|
156
|
+
value={category}
|
|
157
|
+
onChange={(event) => setCategory(event.target.value)}
|
|
158
|
+
>
|
|
159
|
+
<option value="">{form.noCategory}</option>
|
|
160
|
+
{props.categories.map((candidate) => (
|
|
161
|
+
<option key={candidate.key} value={candidate.key}>
|
|
162
|
+
{candidate.name}
|
|
163
|
+
</option>
|
|
164
|
+
))}
|
|
165
|
+
</select>
|
|
166
|
+
</label>
|
|
167
|
+
)}
|
|
168
|
+
</div>
|
|
169
|
+
<div className="sc-field-row">
|
|
170
|
+
<label className="sc-field">
|
|
171
|
+
<span className="sc-field-label">{form.startsOn}</span>
|
|
172
|
+
<input
|
|
173
|
+
className="sc-input"
|
|
174
|
+
type="date"
|
|
175
|
+
value={startsOn}
|
|
176
|
+
max={dueOn === "" ? undefined : dueOn}
|
|
177
|
+
onChange={(event) => setStartsOn(event.target.value)}
|
|
178
|
+
/>
|
|
179
|
+
</label>
|
|
120
180
|
<label className="sc-field">
|
|
121
181
|
<span className="sc-field-label">{form.dueOn}</span>
|
|
122
182
|
<input
|
|
123
183
|
className="sc-input"
|
|
124
184
|
type="date"
|
|
125
185
|
value={dueOn}
|
|
186
|
+
min={startsOn === "" ? undefined : startsOn}
|
|
126
187
|
onChange={(event) => setDueOn(event.target.value)}
|
|
127
188
|
/>
|
|
128
189
|
</label>
|
|
@@ -150,9 +211,23 @@ export function GoalForm(props: GoalFormProps) {
|
|
|
150
211
|
);
|
|
151
212
|
}
|
|
152
213
|
|
|
214
|
+
/**
|
|
215
|
+
* The month a goal's calendar opens on: the month it starts while that is still to come, the
|
|
216
|
+
* month it is meant to be reached by once that is past, and this month otherwise.
|
|
217
|
+
*/
|
|
218
|
+
export function goalMonth(goal: RestGoal, today: string): string {
|
|
219
|
+
if (goal.startsOn !== null && today < goal.startsOn) {
|
|
220
|
+
return monthOf(goal.startsOn);
|
|
221
|
+
}
|
|
222
|
+
if (goal.dueOn !== null && today > goal.dueOn) {
|
|
223
|
+
return monthOf(goal.dueOn);
|
|
224
|
+
}
|
|
225
|
+
return monthOf(today);
|
|
226
|
+
}
|
|
227
|
+
|
|
153
228
|
export interface GoalViewProps {
|
|
154
229
|
readonly goal: RestGoal;
|
|
155
|
-
/** The tasks of the board; those that work towards the goal are shown
|
|
230
|
+
/** The tasks of the board; those that work towards the goal are shown. */
|
|
156
231
|
readonly tasks: readonly RestTask[];
|
|
157
232
|
readonly taskHref: (task: RestTask) => string;
|
|
158
233
|
/** Where a document of the project is read, by its path, for the links in what was written. */
|
|
@@ -163,6 +238,16 @@ export interface GoalViewProps {
|
|
|
163
238
|
/** Called when an owner moves the goal to another state from here. */
|
|
164
239
|
readonly onMove?: ((goal: RestGoal, state: GoalState) => void) | undefined;
|
|
165
240
|
readonly busy?: boolean | undefined;
|
|
241
|
+
/** Which view of the goal's tasks is shown: the list when left out. */
|
|
242
|
+
readonly view?: GoalTaskView | undefined;
|
|
243
|
+
/** Where each view is; left out, the views cannot be switched from here. */
|
|
244
|
+
readonly viewHref?: ((view: GoalTaskView) => string) | undefined;
|
|
245
|
+
/** The board of the goal's tasks, built by the composition from the board's own parts; the list stands in without it. */
|
|
246
|
+
readonly board?: ReactNode;
|
|
247
|
+
/** The calendar of the goal's tasks, the same way. */
|
|
248
|
+
readonly calendar?: ReactNode;
|
|
249
|
+
/** Called to put every done or dropped task of the goal away (ADR-0029); left out, it is not offered. */
|
|
250
|
+
readonly onArchiveTasks?: (() => void) | undefined;
|
|
166
251
|
}
|
|
167
252
|
|
|
168
253
|
export function GoalView(props: GoalViewProps) {
|
|
@@ -170,95 +255,156 @@ export function GoalView(props: GoalViewProps) {
|
|
|
170
255
|
const roadmap = words.roadmap;
|
|
171
256
|
const { goal } = props;
|
|
172
257
|
const towards = props.tasks.filter((task) => task.goalId === goal.id);
|
|
258
|
+
const view: GoalTaskView = props.view ?? "list";
|
|
259
|
+
const { viewHref } = props;
|
|
260
|
+
const done = goal.taskCounts.done;
|
|
261
|
+
const total = Object.values(goal.taskCounts).reduce((sum, n) => sum + n, 0);
|
|
262
|
+
const list =
|
|
263
|
+
towards.length === 0 ? (
|
|
264
|
+
<p className="sc-muted">{roadmap.noTasks}</p>
|
|
265
|
+
) : (
|
|
266
|
+
TASK_STATES.map((state) => {
|
|
267
|
+
const inState = towards.filter((task) => task.state === state);
|
|
268
|
+
return inState.length === 0 ? null : (
|
|
269
|
+
<div key={state} className="sc-goal-tasks">
|
|
270
|
+
<h3 className="sc-goal-tasks-state">
|
|
271
|
+
{words.vocabulary.state[state]}{" "}
|
|
272
|
+
<span className="sc-column-count">{inState.length}</span>
|
|
273
|
+
</h3>
|
|
274
|
+
<TaskList tasks={inState} taskHref={props.taskHref} today={props.today} />
|
|
275
|
+
</div>
|
|
276
|
+
);
|
|
277
|
+
})
|
|
278
|
+
);
|
|
279
|
+
const shown =
|
|
280
|
+
view === "board" && props.board !== undefined
|
|
281
|
+
? props.board
|
|
282
|
+
: view === "calendar" && props.calendar !== undefined
|
|
283
|
+
? props.calendar
|
|
284
|
+
: list;
|
|
173
285
|
return (
|
|
174
|
-
<div className="sc-
|
|
175
|
-
<header className="sc-
|
|
176
|
-
<
|
|
177
|
-
|
|
178
|
-
<
|
|
179
|
-
|
|
180
|
-
<
|
|
181
|
-
|
|
286
|
+
<div className="sc-goal-page">
|
|
287
|
+
<header className="sc-goal-header">
|
|
288
|
+
<div className="sc-task-header">
|
|
289
|
+
<h1 className="sc-task-title">{goal.title}</h1>
|
|
290
|
+
<div className="sc-task-badges">
|
|
291
|
+
{goal.category !== null && <CategoryBadge category={goal.category} />}
|
|
292
|
+
<GoalStateBadge state={goal.state} />
|
|
293
|
+
{goal.dueOn !== null && (
|
|
294
|
+
<DueBadge dueOn={goal.dueOn} state={dueState(goal)} today={props.today} />
|
|
295
|
+
)}
|
|
296
|
+
</div>
|
|
182
297
|
</div>
|
|
183
|
-
|
|
184
|
-
<div className="sc-task-body">
|
|
185
|
-
<div className="sc-task-main">
|
|
298
|
+
<div className="sc-goal-progress">
|
|
186
299
|
<ProgressBar counts={goal.taskCounts} />
|
|
187
|
-
{
|
|
188
|
-
<Markdown source={goal.body} docHref={props.docHref} />
|
|
189
|
-
) : (
|
|
190
|
-
<p className="sc-muted">{roadmap.nothingMore}</p>
|
|
191
|
-
)}
|
|
192
|
-
<section className="sc-task-section" aria-label={roadmap.tasks}>
|
|
193
|
-
<h2 className="sc-section-title">{roadmap.tasks}</h2>
|
|
194
|
-
{towards.length === 0 ? (
|
|
195
|
-
<p className="sc-muted">{roadmap.noTasks}</p>
|
|
196
|
-
) : (
|
|
197
|
-
TASK_STATES.map((state) => {
|
|
198
|
-
const inState = towards.filter((task) => task.state === state);
|
|
199
|
-
return inState.length === 0 ? null : (
|
|
200
|
-
<div key={state} className="sc-goal-tasks">
|
|
201
|
-
<h3 className="sc-goal-tasks-state">
|
|
202
|
-
{words.vocabulary.state[state]}{" "}
|
|
203
|
-
<span className="sc-column-count">{inState.length}</span>
|
|
204
|
-
</h3>
|
|
205
|
-
<TaskList tasks={inState} taskHref={props.taskHref} today={props.today} />
|
|
206
|
-
</div>
|
|
207
|
-
);
|
|
208
|
-
})
|
|
209
|
-
)}
|
|
210
|
-
</section>
|
|
300
|
+
<span className="sc-muted">{roadmap.progress(done, total)}</span>
|
|
211
301
|
</div>
|
|
212
|
-
<
|
|
213
|
-
<
|
|
214
|
-
<
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
{
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
<
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
{
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
<
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
<
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
302
|
+
<dl className="sc-facts-row">
|
|
303
|
+
<div>
|
|
304
|
+
<dt>{roadmap.state}</dt>
|
|
305
|
+
<dd>
|
|
306
|
+
{props.onMove === undefined ? (
|
|
307
|
+
words.vocabulary.goalState[goal.state]
|
|
308
|
+
) : (
|
|
309
|
+
<select
|
|
310
|
+
className="sc-input"
|
|
311
|
+
aria-label={roadmap.state}
|
|
312
|
+
value={goal.state}
|
|
313
|
+
disabled={props.busy === true}
|
|
314
|
+
onChange={(event) => props.onMove?.(goal, event.target.value as GoalState)}
|
|
315
|
+
>
|
|
316
|
+
{GOAL_STATES.map((state) => (
|
|
317
|
+
<option key={state} value={state}>
|
|
318
|
+
{words.vocabulary.goalState[state]}
|
|
319
|
+
</option>
|
|
320
|
+
))}
|
|
321
|
+
</select>
|
|
322
|
+
)}
|
|
323
|
+
</dd>
|
|
324
|
+
</div>
|
|
325
|
+
<div>
|
|
326
|
+
<dt>{roadmap.category}</dt>
|
|
327
|
+
<dd>
|
|
328
|
+
{goal.category === null ? (
|
|
329
|
+
<span className="sc-muted">{roadmap.noCategory}</span>
|
|
330
|
+
) : (
|
|
331
|
+
<CategoryBadge category={goal.category} />
|
|
332
|
+
)}
|
|
333
|
+
</dd>
|
|
334
|
+
</div>
|
|
335
|
+
<div>
|
|
336
|
+
<dt>{roadmap.starts}</dt>
|
|
337
|
+
<dd>
|
|
338
|
+
{goal.startsOn === null ? (
|
|
339
|
+
<span className="sc-muted">{roadmap.noStart}</span>
|
|
340
|
+
) : (
|
|
341
|
+
<Day day={goal.startsOn} />
|
|
342
|
+
)}
|
|
343
|
+
</dd>
|
|
344
|
+
</div>
|
|
345
|
+
<div>
|
|
346
|
+
<dt>{roadmap.target}</dt>
|
|
347
|
+
<dd>
|
|
348
|
+
{goal.dueOn === null ? (
|
|
349
|
+
<span className="sc-muted">{roadmap.noTarget}</span>
|
|
350
|
+
) : (
|
|
351
|
+
<Day day={goal.dueOn} />
|
|
352
|
+
)}
|
|
353
|
+
</dd>
|
|
354
|
+
</div>
|
|
355
|
+
<div>
|
|
356
|
+
<dt>{roadmap.setBy}</dt>
|
|
357
|
+
<dd>
|
|
358
|
+
<PersonChip person={goal.createdBy} /> <Time iso={goal.createdAt} />
|
|
359
|
+
</dd>
|
|
360
|
+
</div>
|
|
253
361
|
{props.editHref !== undefined && (
|
|
254
|
-
<div
|
|
255
|
-
<a className="sc-button sc-button-secondary" href={props.editHref}>
|
|
362
|
+
<div>
|
|
363
|
+
<a className="sc-button sc-button-secondary sc-button-sm" href={props.editHref}>
|
|
256
364
|
{words.common.edit}
|
|
257
365
|
</a>
|
|
258
366
|
</div>
|
|
259
367
|
)}
|
|
260
|
-
|
|
261
|
-
|
|
368
|
+
{props.onArchiveTasks !== undefined && (
|
|
369
|
+
<div>
|
|
370
|
+
<Button
|
|
371
|
+
variant="secondary"
|
|
372
|
+
size="sm"
|
|
373
|
+
disabled={props.busy === true}
|
|
374
|
+
onClick={props.onArchiveTasks}
|
|
375
|
+
>
|
|
376
|
+
{roadmap.archiveTasks}
|
|
377
|
+
</Button>
|
|
378
|
+
</div>
|
|
379
|
+
)}
|
|
380
|
+
</dl>
|
|
381
|
+
{goal.body.length > 0 && (
|
|
382
|
+
<details className="sc-goal-about">
|
|
383
|
+
<summary className="sc-goal-about-summary">{roadmap.about}</summary>
|
|
384
|
+
<Markdown source={goal.body} docHref={props.docHref} />
|
|
385
|
+
</details>
|
|
386
|
+
)}
|
|
387
|
+
</header>
|
|
388
|
+
{viewHref !== undefined && (
|
|
389
|
+
<nav className="sc-views" aria-label={roadmap.view}>
|
|
390
|
+
{GOAL_TASK_VIEWS.map((candidate) => (
|
|
391
|
+
<a
|
|
392
|
+
key={candidate}
|
|
393
|
+
className={cx("sc-view", candidate === view && "sc-view-current")}
|
|
394
|
+
href={viewHref(candidate)}
|
|
395
|
+
aria-current={candidate === view ? "page" : undefined}
|
|
396
|
+
>
|
|
397
|
+
{roadmap.views[candidate]}
|
|
398
|
+
</a>
|
|
399
|
+
))}
|
|
400
|
+
</nav>
|
|
401
|
+
)}
|
|
402
|
+
<section
|
|
403
|
+
className={cx("sc-goal-tasks-view", `sc-goal-view-${view}`)}
|
|
404
|
+
aria-label={roadmap.tasks}
|
|
405
|
+
>
|
|
406
|
+
{shown}
|
|
407
|
+
</section>
|
|
262
408
|
</div>
|
|
263
409
|
);
|
|
264
410
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import ReactMarkdown from "react-markdown";
|
|
2
2
|
import remarkGfm from "remark-gfm";
|
|
3
3
|
import { isDocumentPath } from "../documents.js";
|
|
4
|
+
import { type Picture, PictureThumb } from "./pictures.js";
|
|
4
5
|
|
|
5
6
|
// What people and agents write is untrusted. It is rendered as React elements, never as HTML:
|
|
6
7
|
// raw HTML in the source stays text, links are limited to a few schemes and open elsewhere,
|
|
@@ -20,6 +21,11 @@ export interface MarkdownProps {
|
|
|
20
21
|
* is neither one of those nor over https stays text.
|
|
21
22
|
*/
|
|
22
23
|
readonly fileSrc?: ((source: string) => string | undefined) | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* Called when a picture that came with the text is pressed, to show it large over the page;
|
|
26
|
+
* left out, such a picture is only seen where it is.
|
|
27
|
+
*/
|
|
28
|
+
readonly onOpenPicture?: ((picture: Picture, opener: HTMLElement) => void) | undefined;
|
|
23
29
|
}
|
|
24
30
|
|
|
25
31
|
export function Markdown(props: MarkdownProps) {
|
|
@@ -60,6 +66,19 @@ export function Markdown(props: MarkdownProps) {
|
|
|
60
66
|
if (url === undefined) {
|
|
61
67
|
return <span className="sc-prose-image-text">{alt ?? ""}</span>;
|
|
62
68
|
}
|
|
69
|
+
if (
|
|
70
|
+
named !== undefined &&
|
|
71
|
+
props.onOpenPicture !== undefined &&
|
|
72
|
+
typeof src === "string"
|
|
73
|
+
) {
|
|
74
|
+
const name = src.startsWith("./") ? src.slice(2) : src;
|
|
75
|
+
return (
|
|
76
|
+
<PictureThumb
|
|
77
|
+
picture={{ src: named, alt: alt ?? "", name }}
|
|
78
|
+
onOpen={props.onOpenPicture}
|
|
79
|
+
/>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
63
82
|
return (
|
|
64
83
|
<img
|
|
65
84
|
src={url}
|