@skillcdn/console 0.1.24 → 0.3.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 +8 -7
- 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 +1 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +248 -92
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/credentials.d.ts +5 -0
- package/dist/cli/credentials.d.ts.map +1 -1
- package/dist/cli/credentials.js +2 -0
- package/dist/cli/credentials.js.map +1 -1
- package/dist/cli/directory.d.ts +7 -0
- package/dist/cli/directory.d.ts.map +1 -1
- package/dist/cli/directory.js +7 -0
- package/dist/cli/directory.js.map +1 -1
- package/dist/cli/format.d.ts +13 -28
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +74 -274
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +36 -19
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/pack.d.ts +46 -0
- package/dist/cli/pack.d.ts.map +1 -0
- package/dist/cli/pack.js +250 -0
- package/dist/cli/pack.js.map +1 -0
- package/dist/client.d.ts +26 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +14 -1
- package/dist/client.js.map +1 -1
- package/dist/compat.d.ts +1 -1
- package/dist/compat.js +1 -1
- package/dist/components/calendar.d.ts +5 -1
- package/dist/components/calendar.d.ts.map +1 -1
- package/dist/components/calendar.js +3 -3
- package/dist/components/calendar.js.map +1 -1
- package/dist/components/decision-list.d.ts +13 -1
- package/dist/components/decision-list.d.ts.map +1 -1
- package/dist/components/decision-list.js +74 -29
- package/dist/components/decision-list.js.map +1 -1
- package/dist/components/event-feed.d.ts.map +1 -1
- package/dist/components/event-feed.js +2 -0
- package/dist/components/event-feed.js.map +1 -1
- package/dist/components/pack-connect.d.ts +23 -0
- package/dist/components/pack-connect.d.ts.map +1 -0
- package/dist/components/pack-connect.js +86 -0
- package/dist/components/pack-connect.js.map +1 -0
- package/dist/components/pack-token.d.ts +27 -0
- package/dist/components/pack-token.d.ts.map +1 -0
- package/dist/components/pack-token.js +11 -0
- package/dist/components/pack-token.js.map +1 -0
- package/dist/components/paged.d.ts +24 -0
- package/dist/components/paged.d.ts.map +1 -1
- package/dist/components/paged.js +40 -4
- package/dist/components/paged.js.map +1 -1
- package/dist/components/projects.d.ts +13 -0
- package/dist/components/projects.d.ts.map +1 -1
- package/dist/components/projects.js +37 -5
- package/dist/components/projects.js.map +1 -1
- package/dist/components/skills.d.ts.map +1 -1
- package/dist/components/skills.js +8 -4
- package/dist/components/skills.js.map +1 -1
- package/dist/components/ui.d.ts +2 -0
- package/dist/components/ui.d.ts.map +1 -1
- package/dist/components/ui.js +7 -2
- package/dist/components/ui.js.map +1 -1
- package/dist/components/work.d.ts +3 -1
- package/dist/components/work.d.ts.map +1 -1
- package/dist/components/work.js +1 -1
- package/dist/components/work.js.map +1 -1
- package/dist/components/workspace.d.ts +46 -0
- package/dist/components/workspace.d.ts.map +1 -0
- package/dist/components/workspace.js +124 -0
- package/dist/components/workspace.js.map +1 -0
- package/dist/console.css +268 -5
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +62 -23
- 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 +64 -2
- package/dist/data.js.map +1 -1
- package/dist/dates.d.ts +12 -2
- package/dist/dates.d.ts.map +1 -1
- package/dist/dates.js +20 -2
- package/dist/dates.js.map +1 -1
- package/dist/i18n/en.d.ts +66 -0
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +80 -8
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +75 -7
- package/dist/i18n/ko.js.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/router.d.ts +10 -5
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +24 -9
- package/dist/router.js.map +1 -1
- package/dist/routes.d.ts +4 -0
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +4 -0
- package/dist/routes.js.map +1 -1
- package/dist/schemas.d.ts +500 -158
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +169 -165
- package/dist/schemas.js.map +1 -1
- package/dist/vocabulary.d.ts +19 -1
- package/dist/vocabulary.d.ts.map +1 -1
- package/dist/vocabulary.js +16 -0
- package/dist/vocabulary.js.map +1 -1
- package/docs/specs/cli.md +19 -14
- package/docs/specs/rest.md +34 -19
- package/package.json +2 -1
- package/skills/building-a-console/SKILL.md +5 -5
- package/skills/building-a-console/references/the-look-from-a-website.md +39 -0
- package/skills/deploying-a-console/SKILL.md +9 -8
- package/skills/working-the-board/SKILL.md +5 -5
- package/skills/working-the-board/references/setting-up.md +1 -1
- package/skills/working-the-board/references/terms.md +2 -0
- package/skills/working-the-board/references/the-sentence.md +1 -1
- package/src/api.ts +18 -7
- package/src/cli/cli.ts +345 -109
- package/src/cli/credentials.ts +7 -0
- package/src/cli/directory.ts +9 -0
- package/src/cli/format.ts +82 -341
- package/src/cli/help.ts +43 -19
- package/src/cli/pack.ts +356 -0
- package/src/client.ts +84 -0
- package/src/compat.ts +1 -1
- package/src/components/calendar.tsx +8 -2
- package/src/components/decision-list.tsx +227 -94
- package/src/components/event-feed.tsx +2 -0
- package/src/components/pack-connect.tsx +179 -0
- package/src/components/pack-token.tsx +74 -0
- package/src/components/paged.tsx +72 -3
- package/src/components/projects.tsx +108 -9
- package/src/components/skills.tsx +51 -33
- package/src/components/ui.tsx +16 -3
- package/src/components/work.tsx +4 -1
- package/src/components/workspace.tsx +298 -0
- package/src/console.tsx +176 -29
- package/src/data.ts +89 -2
- package/src/dates.ts +23 -2
- package/src/i18n/en.ts +94 -8
- package/src/i18n/ko.ts +84 -7
- package/src/index.ts +25 -1
- package/src/router.ts +37 -14
- package/src/routes.ts +4 -0
- package/src/schemas.ts +202 -189
- package/src/styles/console.css +268 -5
- package/src/vocabulary.ts +25 -0
package/src/components/ui.tsx
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
type TaskPriority,
|
|
10
10
|
type TaskState,
|
|
11
11
|
} from "../api.js";
|
|
12
|
-
import { daysBetween, formatDay, today } from "../dates.js";
|
|
12
|
+
import { dayOf, daysBetween, formatDay, today } from "../dates.js";
|
|
13
13
|
import { useLanguage, useWords } from "../i18n/index.js";
|
|
14
14
|
|
|
15
15
|
// Small building blocks. Each one is a class in styles/console.css and nothing more. Classes
|
|
@@ -356,11 +356,24 @@ export function formatBytes(size: number): string {
|
|
|
356
356
|
return `${mb < 10 ? mb.toFixed(1) : Math.round(mb)} MB`;
|
|
357
357
|
}
|
|
358
358
|
|
|
359
|
-
export function Time(props: {
|
|
359
|
+
export function Time(props: {
|
|
360
|
+
readonly iso: string;
|
|
361
|
+
/** `day` shows the day alone, with the moment as the title, where a row has no room for the hour. */
|
|
362
|
+
readonly precision?: "minute" | "day";
|
|
363
|
+
}) {
|
|
360
364
|
const { tag } = useLanguage();
|
|
365
|
+
const full = formatInstant(props.iso, tag);
|
|
366
|
+
if (props.precision === "day") {
|
|
367
|
+
const date = new Date(props.iso);
|
|
368
|
+
return (
|
|
369
|
+
<time className="sc-time" dateTime={props.iso} title={full}>
|
|
370
|
+
{Number.isNaN(date.getTime()) ? props.iso : formatDay(dayOf(date), tag)}
|
|
371
|
+
</time>
|
|
372
|
+
);
|
|
373
|
+
}
|
|
361
374
|
return (
|
|
362
375
|
<time className="sc-time" dateTime={props.iso}>
|
|
363
|
-
{
|
|
376
|
+
{full}
|
|
364
377
|
</time>
|
|
365
378
|
);
|
|
366
379
|
}
|
package/src/components/work.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { RestTask, RestWork } from "../api.js";
|
|
1
|
+
import type { RestTask, RestWork, WeekStart } from "../api.js";
|
|
2
2
|
import { addDays, today as todayOf } from "../dates.js";
|
|
3
3
|
import { useWords } from "../i18n/index.js";
|
|
4
4
|
import { Week } from "./calendar.js";
|
|
@@ -28,6 +28,8 @@ export interface WorkListProps {
|
|
|
28
28
|
readonly decisionHref: (projectKey: string, decisionId: string) => string;
|
|
29
29
|
/** Where the page is with the week of a day at the top. */
|
|
30
30
|
readonly weekHref: (day: string) => string;
|
|
31
|
+
/** The day the week begins on, as the workspace says (ADR-0035); the language's when left out. */
|
|
32
|
+
readonly weekStart?: WeekStart | undefined;
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
export function WorkList(props: WorkListProps) {
|
|
@@ -70,6 +72,7 @@ export function WorkList(props: WorkListProps) {
|
|
|
70
72
|
taskHref={hrefOf}
|
|
71
73
|
weekHref={props.weekHref}
|
|
72
74
|
markOf={markOf}
|
|
75
|
+
weekStart={props.weekStart}
|
|
73
76
|
/>
|
|
74
77
|
<div className="sc-work-lists">
|
|
75
78
|
{groups
|
|
@@ -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
|
+
}
|