@skillcdn/console 0.3.0 → 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.
Files changed (129) hide show
  1. package/README.md +3 -3
  2. package/dist/api.d.ts +2 -2
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/api.js +2 -2
  5. package/dist/api.js.map +1 -1
  6. package/dist/cli/cli.d.ts +1 -1
  7. package/dist/cli/cli.d.ts.map +1 -1
  8. package/dist/cli/cli.js +185 -28
  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 +67 -13
  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 +30 -13
  24. package/dist/cli/help.js.map +1 -1
  25. package/dist/client.d.ts +26 -1
  26. package/dist/client.d.ts.map +1 -1
  27. package/dist/client.js +14 -1
  28. package/dist/client.js.map +1 -1
  29. package/dist/components/calendar.d.ts +5 -1
  30. package/dist/components/calendar.d.ts.map +1 -1
  31. package/dist/components/calendar.js +3 -3
  32. package/dist/components/calendar.js.map +1 -1
  33. package/dist/components/event-feed.d.ts.map +1 -1
  34. package/dist/components/event-feed.js +2 -0
  35. package/dist/components/event-feed.js.map +1 -1
  36. package/dist/components/pack-connect.d.ts +23 -0
  37. package/dist/components/pack-connect.d.ts.map +1 -0
  38. package/dist/components/pack-connect.js +86 -0
  39. package/dist/components/pack-connect.js.map +1 -0
  40. package/dist/components/pack-token.d.ts +27 -0
  41. package/dist/components/pack-token.d.ts.map +1 -0
  42. package/dist/components/pack-token.js +11 -0
  43. package/dist/components/pack-token.js.map +1 -0
  44. package/dist/components/projects.d.ts +13 -0
  45. package/dist/components/projects.d.ts.map +1 -1
  46. package/dist/components/projects.js +37 -5
  47. package/dist/components/projects.js.map +1 -1
  48. package/dist/components/work.d.ts +3 -1
  49. package/dist/components/work.d.ts.map +1 -1
  50. package/dist/components/work.js +1 -1
  51. package/dist/components/work.js.map +1 -1
  52. package/dist/components/workspace.d.ts +46 -0
  53. package/dist/components/workspace.d.ts.map +1 -0
  54. package/dist/components/workspace.js +124 -0
  55. package/dist/components/workspace.js.map +1 -0
  56. package/dist/console.css +156 -0
  57. package/dist/console.d.ts.map +1 -1
  58. package/dist/console.js +60 -21
  59. package/dist/console.js.map +1 -1
  60. package/dist/data.d.ts +16 -1
  61. package/dist/data.d.ts.map +1 -1
  62. package/dist/data.js +37 -0
  63. package/dist/data.js.map +1 -1
  64. package/dist/dates.d.ts +12 -2
  65. package/dist/dates.d.ts.map +1 -1
  66. package/dist/dates.js +20 -2
  67. package/dist/dates.js.map +1 -1
  68. package/dist/i18n/en.d.ts +53 -0
  69. package/dist/i18n/en.d.ts.map +1 -1
  70. package/dist/i18n/en.js +65 -8
  71. package/dist/i18n/en.js.map +1 -1
  72. package/dist/i18n/ko.d.ts.map +1 -1
  73. package/dist/i18n/ko.js +60 -7
  74. package/dist/i18n/ko.js.map +1 -1
  75. package/dist/index.d.ts +3 -0
  76. package/dist/index.d.ts.map +1 -1
  77. package/dist/index.js +3 -0
  78. package/dist/index.js.map +1 -1
  79. package/dist/router.d.ts +4 -4
  80. package/dist/router.d.ts.map +1 -1
  81. package/dist/router.js +11 -2
  82. package/dist/router.js.map +1 -1
  83. package/dist/routes.d.ts +4 -0
  84. package/dist/routes.d.ts.map +1 -1
  85. package/dist/routes.js +4 -0
  86. package/dist/routes.js.map +1 -1
  87. package/dist/schemas.d.ts +182 -2
  88. package/dist/schemas.d.ts.map +1 -1
  89. package/dist/schemas.js +110 -13
  90. package/dist/schemas.js.map +1 -1
  91. package/dist/vocabulary.d.ts +19 -1
  92. package/dist/vocabulary.d.ts.map +1 -1
  93. package/dist/vocabulary.js +16 -0
  94. package/dist/vocabulary.js.map +1 -1
  95. package/docs/specs/cli.md +16 -11
  96. package/docs/specs/rest.md +31 -8
  97. package/package.json +2 -2
  98. package/skills/building-a-console/SKILL.md +5 -5
  99. package/skills/building-a-console/references/the-look-from-a-website.md +39 -0
  100. package/skills/deploying-a-console/SKILL.md +8 -7
  101. package/skills/working-the-board/SKILL.md +2 -2
  102. package/skills/working-the-board/references/setting-up.md +1 -1
  103. package/skills/working-the-board/references/terms.md +2 -0
  104. package/skills/working-the-board/references/the-sentence.md +1 -1
  105. package/src/api.ts +16 -0
  106. package/src/cli/cli.ts +253 -33
  107. package/src/cli/credentials.ts +7 -0
  108. package/src/cli/directory.ts +9 -0
  109. package/src/cli/format.ts +72 -13
  110. package/src/cli/help.ts +36 -13
  111. package/src/client.ts +84 -0
  112. package/src/components/calendar.tsx +8 -2
  113. package/src/components/event-feed.tsx +2 -0
  114. package/src/components/pack-connect.tsx +179 -0
  115. package/src/components/pack-token.tsx +74 -0
  116. package/src/components/projects.tsx +108 -9
  117. package/src/components/work.tsx +4 -1
  118. package/src/components/workspace.tsx +298 -0
  119. package/src/console.tsx +173 -29
  120. package/src/data.ts +57 -0
  121. package/src/dates.ts +23 -2
  122. package/src/i18n/en.ts +76 -8
  123. package/src/i18n/ko.ts +67 -7
  124. package/src/index.ts +16 -0
  125. package/src/router.ts +17 -5
  126. package/src/routes.ts +4 -0
  127. package/src/schemas.ts +129 -12
  128. package/src/styles/console.css +156 -0
  129. package/src/vocabulary.ts +25 -0
package/src/console.tsx CHANGED
@@ -35,6 +35,7 @@ import {
35
35
  RULES_PATH,
36
36
  RULES_TITLE,
37
37
  type TaskState,
38
+ type WeekStart,
38
39
  } from "./api.js";
39
40
  import { Board, BoardFilters, type BoardProps, byDue, filterTasks } from "./components/board.js";
40
41
  import { Calendar, type CalendarProps, Workload } from "./components/calendar.js";
@@ -44,12 +45,12 @@ import { DecisionForm } from "./components/decision-form.js";
44
45
  import { DecisionCard, DecisionList, type DecisionListProps } from "./components/decision-list.js";
45
46
  import { EventFeed, type EventFeedProps } from "./components/event-feed.js";
46
47
  import { GoalForm, GoalList, type GoalListProps, GoalView, goalMonth } from "./components/goals.js";
48
+ import { PackTokenWays } from "./components/pack-token.js";
47
49
  import { PAGE_SIZE } from "./components/paged.js";
48
50
  import { ListingForm, PeopleList, type PeopleListProps } from "./components/people.js";
49
51
  import {
50
52
  MemberList,
51
53
  type MemberListProps,
52
- PackTokenForm,
53
54
  ProjectForm,
54
55
  ProjectList,
55
56
  type ProjectListProps,
@@ -81,6 +82,12 @@ import {
81
82
  Time,
82
83
  } from "./components/ui.js";
83
84
  import { WorkList, type WorkListProps } from "./components/work.js";
85
+ import {
86
+ peopleVisibilityOf,
87
+ WorkspaceSettingsForm,
88
+ WorkspaceSettingsView,
89
+ weekStartOf,
90
+ } from "./components/workspace.js";
84
91
  import { ConnectPage } from "./connect-page.js";
85
92
  import { type ConsoleData, useConsoleData } from "./data.js";
86
93
  import { monthOf, today } from "./dates.js";
@@ -861,6 +868,7 @@ function ProjectPage(props: {
861
868
  tasks={shown}
862
869
  month={month}
863
870
  today={now}
871
+ weekStart={weekStartOf(data.me?.workspace)}
864
872
  categories={data.categories}
865
873
  goals={data.goals}
866
874
  taskHref={href}
@@ -1075,6 +1083,7 @@ function ProjectPage(props: {
1075
1083
  tasks={towards}
1076
1084
  month={month}
1077
1085
  today={now}
1086
+ weekStart={weekStartOf(data.me?.workspace)}
1078
1087
  categories={data.categories}
1079
1088
  goals={[goal]}
1080
1089
  taskHref={href}
@@ -1220,19 +1229,34 @@ function ProjectPage(props: {
1220
1229
  const me = data.me?.person ?? undefined;
1221
1230
  const configures = owner;
1222
1231
  const members = words.projects.members;
1232
+ // In a project open to the workspace everyone is a member already (ADR-0035): the list is
1233
+ // everyone, nobody is added or removed, and an owner is made from a row. Where the People
1234
+ // page shows a member only whom they share a project with, an owner lists someone the list
1235
+ // does not show by their login.
1236
+ const open = project.visibility === "workspace";
1237
+ const narrowed =
1238
+ peopleVisibilityOf(data.me?.workspace) === "shared_projects" && me?.role !== "admin";
1223
1239
  return (
1224
1240
  <>
1225
1241
  <div className="sc-page-head">
1226
1242
  <h1 className="sc-page-title">{members.title}</h1>
1227
1243
  </div>
1228
- <p className="sc-lead">{words.vocabulary.visibility[project.visibility]}</p>
1244
+ {open && <p className="sc-lead">{members.open}</p>}
1229
1245
  {error !== undefined && <Callout tone="danger">{error}</Callout>}
1230
1246
  <components.MemberList
1231
1247
  members={data.members}
1232
1248
  people={data.people.filter(inWorkspace)}
1233
1249
  me={me}
1250
+ open={open}
1234
1251
  busy={busy}
1235
- onAdd={configures ? (input) => void act(() => data.actions.addMember(input)) : undefined}
1252
+ onAdd={
1253
+ configures && !open
1254
+ ? (input) => void act(() => data.actions.addMember(input))
1255
+ : undefined
1256
+ }
1257
+ lookup={
1258
+ configures && !open && narrowed ? (login) => data.actions.findPerson(login) : undefined
1259
+ }
1236
1260
  onChangeRole={
1237
1261
  configures
1238
1262
  ? (member, role) =>
@@ -1240,7 +1264,7 @@ function ProjectPage(props: {
1240
1264
  : undefined
1241
1265
  }
1242
1266
  onRemove={
1243
- configures
1267
+ configures && !open
1244
1268
  ? (member) => void act(() => data.actions.removeMember(member.person.id))
1245
1269
  : undefined
1246
1270
  }
@@ -1382,28 +1406,33 @@ function ProjectSettingsPage(props: {
1382
1406
  <>
1383
1407
  {head}
1384
1408
  {error !== undefined && <Callout tone="danger">{error}</Callout>}
1385
- <section className="sc-panel sc-panel-form" aria-label={words.projects.packToken.aria}>
1386
- <PackTokenForm
1387
- set={project.packToken !== null}
1388
- busy={busy}
1389
- onSubmit={(token) =>
1390
- void act(async () => {
1391
- await data.actions.setPackToken(token);
1392
- navigation.replace(here);
1393
- })
1394
- }
1395
- onRemove={
1396
- project.packToken === null
1397
- ? undefined
1398
- : () =>
1399
- void act(async () => {
1400
- await data.actions.removePackToken();
1401
- navigation.replace(here);
1402
- })
1403
- }
1404
- onCancel={() => navigation.back(here)}
1405
- />
1406
- </section>
1409
+ <PackTokenWays
1410
+ title={settings.packToken}
1411
+ intro={words.projects.packToken.intro}
1412
+ set={project.packToken !== null}
1413
+ setAt={project.packToken?.setAt}
1414
+ busy={busy}
1415
+ onStart={data.actions.connectPackToken}
1416
+ poll={data.actions.packTokenConnection}
1417
+ onCancelConnect={data.actions.cancelPackTokenConnection}
1418
+ onConnected={() => navigation.replace(here)}
1419
+ onSubmit={(token) =>
1420
+ void act(async () => {
1421
+ await data.actions.setPackToken(token);
1422
+ navigation.replace(here);
1423
+ })
1424
+ }
1425
+ onRemove={
1426
+ project.packToken === null
1427
+ ? undefined
1428
+ : () =>
1429
+ void act(async () => {
1430
+ await data.actions.removePackToken();
1431
+ navigation.replace(here);
1432
+ })
1433
+ }
1434
+ onBack={() => navigation.back(here)}
1435
+ />
1407
1436
  </>
1408
1437
  );
1409
1438
  }
@@ -1455,7 +1484,9 @@ function ProjectSettingsPage(props: {
1455
1484
  {owner && (
1456
1485
  <>
1457
1486
  {" "}
1458
- <a href={projectSettingsHref(key, "pack-token")}>{settings.packTokenEdit}</a>
1487
+ <a href={projectSettingsHref(key, "pack-token")}>
1488
+ {project.packToken === null ? settings.packTokenAdd : settings.packTokenEdit}
1489
+ </a>
1459
1490
  </>
1460
1491
  )}
1461
1492
  </dd>
@@ -1516,7 +1547,7 @@ function ProjectSettingsPage(props: {
1516
1547
  * from, which whoever runs the console set. Read by everyone; the rules written by an administrator.
1517
1548
  */
1518
1549
  function WorkspaceSettingsPage(props: {
1519
- readonly form: "rules" | undefined;
1550
+ readonly form: "rules" | "workspace" | "pack-token" | undefined;
1520
1551
  readonly data: ConsoleData;
1521
1552
  readonly navigation: Navigation;
1522
1553
  }) {
@@ -1524,7 +1555,8 @@ function WorkspaceSettingsPage(props: {
1524
1555
  const { form, data, navigation } = props;
1525
1556
  const { busy, error, act } = useAction();
1526
1557
  const administrator = data.me?.person?.role === "admin";
1527
- const skills = data.me?.workspace.skills;
1558
+ const workspace = data.me?.workspace;
1559
+ const skills = workspace?.skills;
1528
1560
  const [rules, setRules] = useState<RestRules | undefined>(undefined);
1529
1561
  useEffect(() => {
1530
1562
  const controller = new AbortController();
@@ -1587,10 +1619,90 @@ function WorkspaceSettingsPage(props: {
1587
1619
  </>
1588
1620
  );
1589
1621
  }
1622
+ if (form === "pack-token") {
1623
+ if (!administrator || workspace === undefined) {
1624
+ return <NothingHere />;
1625
+ }
1626
+ const held = workspace.skills.packToken ?? null;
1627
+ return (
1628
+ <>
1629
+ {head}
1630
+ {error !== undefined && <Callout tone="danger">{error}</Callout>}
1631
+ <PackTokenWays
1632
+ title={words.settings.skillsToken}
1633
+ intro={words.settings.skillsTokenIntro}
1634
+ set={held !== null}
1635
+ setAt={held?.setAt}
1636
+ busy={busy}
1637
+ onStart={data.actions.connectWorkspacePackToken}
1638
+ poll={data.actions.workspacePackTokenConnection}
1639
+ onCancelConnect={data.actions.cancelWorkspacePackTokenConnection}
1640
+ onConnected={() => navigation.replace(PATHS.settings)}
1641
+ onSubmit={(token) =>
1642
+ void act(async () => {
1643
+ await data.actions.setWorkspacePackToken(token);
1644
+ navigation.replace(PATHS.settings);
1645
+ })
1646
+ }
1647
+ onRemove={
1648
+ held === null
1649
+ ? undefined
1650
+ : () =>
1651
+ void act(async () => {
1652
+ await data.actions.removeWorkspacePackToken();
1653
+ navigation.replace(PATHS.settings);
1654
+ })
1655
+ }
1656
+ onBack={() => navigation.back(PATHS.settings)}
1657
+ />
1658
+ </>
1659
+ );
1660
+ }
1661
+ if (form === "workspace") {
1662
+ if (!administrator || workspace === undefined) {
1663
+ return <NothingHere />;
1664
+ }
1665
+ return (
1666
+ <>
1667
+ {head}
1668
+ {error !== undefined && <Callout tone="danger">{error}</Callout>}
1669
+ <section className="sc-panel sc-panel-form" aria-label={words.settings.workspace.aria}>
1670
+ <h2 className="sc-section-title">{words.settings.workspace.title}</h2>
1671
+ <WorkspaceSettingsForm
1672
+ workspace={workspace}
1673
+ busy={busy}
1674
+ onSubmit={(patch) =>
1675
+ void act(async () => {
1676
+ await data.actions.updateWorkspace(patch);
1677
+ navigation.replace(PATHS.settings);
1678
+ })
1679
+ }
1680
+ onCancel={() => navigation.back(PATHS.settings)}
1681
+ />
1682
+ </section>
1683
+ </>
1684
+ );
1685
+ }
1590
1686
  return (
1591
1687
  <>
1592
1688
  {head}
1593
1689
  {error !== undefined && <Callout tone="danger">{error}</Callout>}
1690
+ {workspace !== undefined && (
1691
+ <section className="sc-panel" aria-label={words.settings.workspace.aria}>
1692
+ <div className="sc-section-head">
1693
+ <h2 className="sc-section-title">{words.settings.workspace.title}</h2>
1694
+ {administrator && (
1695
+ <a
1696
+ className="sc-button sc-button-secondary sc-button-sm"
1697
+ href={settingsHref("workspace")}
1698
+ >
1699
+ {words.common.edit}
1700
+ </a>
1701
+ )}
1702
+ </div>
1703
+ <WorkspaceSettingsView workspace={workspace} />
1704
+ </section>
1705
+ )}
1594
1706
  <section className="sc-panel" aria-label={words.rules.title}>
1595
1707
  <h2 className="sc-section-title">{words.rules.title}</h2>
1596
1708
  {rules === undefined ? (
@@ -1651,6 +1763,30 @@ function WorkspaceSettingsPage(props: {
1651
1763
  )}
1652
1764
  </dd>
1653
1765
  </div>
1766
+ {skills.address !== null && (
1767
+ <div>
1768
+ <dt>{words.settings.skillsToken}</dt>
1769
+ <dd>
1770
+ {skills.packToken == null ? (
1771
+ <span className="sc-muted">{words.projects.settings.packTokenNone}</span>
1772
+ ) : (
1773
+ <>
1774
+ {words.projects.settings.packTokenSet} <Time iso={skills.packToken.setAt} />
1775
+ </>
1776
+ )}
1777
+ {administrator && (
1778
+ <>
1779
+ {" "}
1780
+ <a href={settingsHref("pack-token")}>
1781
+ {skills.packToken == null
1782
+ ? words.projects.settings.packTokenAdd
1783
+ : words.projects.settings.packTokenEdit}
1784
+ </a>
1785
+ </>
1786
+ )}
1787
+ </dd>
1788
+ </div>
1789
+ )}
1654
1790
  </dl>
1655
1791
  <p className="sc-muted">{words.settings.skillsFromConfiguration}</p>
1656
1792
  </section>
@@ -1777,6 +1913,8 @@ function PlatePage(props: {
1777
1913
  readonly empty: { readonly title: string; readonly body: string };
1778
1914
  /** What a refusal says where the plate may not be the asker's to see; the server's words otherwise. */
1779
1915
  readonly refused?: { readonly title: string; readonly body: string } | undefined;
1916
+ /** The day the week begins on, as the workspace says (ADR-0035). */
1917
+ readonly weekStart?: WeekStart | undefined;
1780
1918
  }) {
1781
1919
  const words = useWords();
1782
1920
  const { components, load, couldNotLoad, refused } = props;
@@ -1837,6 +1975,7 @@ function PlatePage(props: {
1837
1975
  <components.WorkList
1838
1976
  work={plate}
1839
1977
  week={props.week}
1978
+ weekStart={props.weekStart}
1840
1979
  taskHref={(projectKey, task) => taskHref(projectKey, task.number)}
1841
1980
  decisionHref={(projectKey, decisionId) => decisionHref(projectKey, decisionId)}
1842
1981
  weekHref={props.weekHref}
@@ -1860,6 +1999,7 @@ function WorkPage(props: {
1860
1999
  load={work}
1861
2000
  title={words.work.title}
1862
2001
  week={props.week}
2002
+ weekStart={weekStartOf(props.data.me?.workspace)}
1863
2003
  weekHref={workHref}
1864
2004
  loading={words.work.loading}
1865
2005
  couldNotLoad={words.work.couldNotLoad}
@@ -1888,6 +2028,7 @@ function PersonWorkPage(props: {
1888
2028
  title={words.people.workOf(name)}
1889
2029
  aside={<LinkButton href={PATHS.people}>{words.nav.people}</LinkButton>}
1890
2030
  week={props.week}
2031
+ weekStart={weekStartOf(data.me?.workspace)}
1891
2032
  weekHref={(day) => personWorkHref(id, day)}
1892
2033
  loading={words.people.loadingWork}
1893
2034
  couldNotLoad={words.people.couldNotLoadWork}
@@ -1958,6 +2099,8 @@ function WorkspacePage(props: {
1958
2099
  const making = route.name === "new-listing" && administrator;
1959
2100
  // An owner sees the work of the people in their projects; the console refuses the rest.
1960
2101
  const oversees = administrator || data.projects.some((project) => project.role === "owner");
2102
+ // The list is what the policy shows (ADR-0035): everyone, or the people one shares a project with.
2103
+ const narrowed = peopleVisibilityOf(data.me?.workspace) === "shared_projects" && !administrator;
1961
2104
  return (
1962
2105
  <>
1963
2106
  <div className="sc-page-head">
@@ -1968,6 +2111,7 @@ function WorkspacePage(props: {
1968
2111
  </LinkButton>
1969
2112
  )}
1970
2113
  </div>
2114
+ {narrowed && <p className="sc-lead">{words.people.narrowed}</p>}
1971
2115
  {error !== undefined && <Callout tone="danger">{error}</Callout>}
1972
2116
  {making && (
1973
2117
  <section className="sc-panel sc-panel-form" aria-label={words.people.letInAria}>
package/src/data.ts CHANGED
@@ -32,6 +32,8 @@ import {
32
32
  type RestMemberPatch,
33
33
  type RestMePatch,
34
34
  type RestOrganization,
35
+ type RestPackConnect,
36
+ type RestPackConnectStarted,
35
37
  type RestPerson,
36
38
  type RestPersonPatch,
37
39
  type RestPersonWork,
@@ -54,6 +56,9 @@ import {
54
56
  type RestVersion,
55
57
  type RestVersions,
56
58
  type RestWork,
59
+ type RestWorkspace,
60
+ type RestWorkspacePackConnect,
61
+ type RestWorkspacePatch,
57
62
  restEventSchema,
58
63
  } from "./api.js";
59
64
 
@@ -80,6 +85,11 @@ export interface ConsoleActions {
80
85
  /** Sets the pack token of the project the page is on, or takes it away; for an owner (ADR-0028). */
81
86
  setPackToken(token: string): Promise<RestProject>;
82
87
  removePackToken(): Promise<RestProject>;
88
+ /** Asks the deployment to connect the project's pack with a code a person approves (ADR-0036); for an owner. */
89
+ connectPackToken(): Promise<RestPackConnectStarted>;
90
+ /** How that connection stands; once connected, the project is the one answered. */
91
+ packTokenConnection(signal?: AbortSignal): Promise<RestPackConnect>;
92
+ cancelPackTokenConnection(): Promise<void>;
83
93
  addMember(input: RestMemberInput): Promise<RestMember>;
84
94
  updateMember(personId: string, patch: RestMemberPatch): Promise<RestMember>;
85
95
  removeMember(personId: string): Promise<void>;
@@ -133,6 +143,16 @@ export interface ConsoleActions {
133
143
  removeListing(id: string): Promise<void>;
134
144
  /** Keeps the person's own choice of language (ADR-0012). */
135
145
  updateMe(patch: RestMePatch): Promise<RestMe>;
146
+ /** Changes the workspace's settings (ADR-0035); for an administrator. `me` says them from then on. */
147
+ updateWorkspace(patch: RestWorkspacePatch): Promise<RestWorkspace>;
148
+ /** The workspace's SkillCDN token (ADR-0036), for an administrator: set, taken away, or connected with a code. */
149
+ setWorkspacePackToken(token: string): Promise<RestWorkspace>;
150
+ removeWorkspacePackToken(): Promise<RestWorkspace>;
151
+ connectWorkspacePackToken(): Promise<RestPackConnectStarted>;
152
+ workspacePackTokenConnection(signal?: AbortSignal): Promise<RestWorkspacePackConnect>;
153
+ cancelWorkspacePackTokenConnection(): Promise<void>;
154
+ /** One person of the workspace by their exact login, or nobody: what an owner lists someone the People page does not show by. */
155
+ findPerson(login: string, signal?: AbortSignal): Promise<RestPerson | undefined>;
136
156
  /** Marks a run that will not come back as abandoned. */
137
157
  abandonRun(id: string): Promise<RestRun>;
138
158
  /** Everything that happened to one task of the project, oldest first. */
@@ -538,6 +558,15 @@ export function useConsoleData(client: ConsoleClient, projectKey: string | undef
538
558
  setProject(changed);
539
559
  return changed;
540
560
  },
561
+ connectPackToken: () => scope().connectPackToken(),
562
+ async packTokenConnection(signal) {
563
+ const state = await scope().packTokenConnection(signal);
564
+ if (state.status === "connected") {
565
+ setProject(state.project);
566
+ }
567
+ return state;
568
+ },
569
+ cancelPackTokenConnection: () => scope().cancelPackTokenConnection(),
541
570
  async addMember(input) {
542
571
  const member = await scope().addMember(input);
543
572
  await refreshProject();
@@ -649,6 +678,34 @@ export function useConsoleData(client: ConsoleClient, projectKey: string | undef
649
678
  setMe(answer);
650
679
  return answer;
651
680
  },
681
+ async updateWorkspace(patch) {
682
+ const workspace = await client.updateWorkspace(patch);
683
+ setMe((current) => (current === undefined ? undefined : { ...current, workspace }));
684
+ // The member visibility may have changed: the people are read again under the new setting.
685
+ await refreshWorkspace();
686
+ return workspace;
687
+ },
688
+ async setWorkspacePackToken(token) {
689
+ const workspace = await client.setWorkspacePackToken({ token });
690
+ setMe((current) => (current === undefined ? undefined : { ...current, workspace }));
691
+ return workspace;
692
+ },
693
+ async removeWorkspacePackToken() {
694
+ const workspace = await client.removeWorkspacePackToken();
695
+ setMe((current) => (current === undefined ? undefined : { ...current, workspace }));
696
+ return workspace;
697
+ },
698
+ connectWorkspacePackToken: () => client.connectWorkspacePackToken(),
699
+ async workspacePackTokenConnection(signal) {
700
+ const state = await client.workspacePackTokenConnection(signal);
701
+ if (state.status === "connected") {
702
+ const { workspace } = state;
703
+ setMe((current) => (current === undefined ? undefined : { ...current, workspace }));
704
+ }
705
+ return state;
706
+ },
707
+ cancelWorkspacePackTokenConnection: () => client.cancelWorkspacePackTokenConnection(),
708
+ findPerson: (login, signal) => client.findPerson(login, signal),
652
709
  async abandonRun(id) {
653
710
  const run = await scope().endRun(id, { status: "abandoned" });
654
711
  await refreshProject();
package/src/dates.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type { WeekStart } from "./vocabulary.js";
2
+
1
3
  // Days, as the plan is made in (ADR-0019): `2026-10-20`, read and written as the strings they
2
4
  // are, so that a deadline is the same day wherever the people are. The browser's own clock says
3
5
  // which day is today; nothing here looks at it unless asked.
@@ -62,8 +64,27 @@ export function addMonths(month: string, count: number): string {
62
64
  return `${String(date.getFullYear()).padStart(4, "0")}-${String(date.getMonth() + 1).padStart(2, "0")}`;
63
65
  }
64
66
 
65
- /** The weekday a week begins on where the language is spoken: 1 for Monday, 7 for Sunday. */
66
- export function firstDayOfWeek(language: string): number {
67
+ /**
68
+ * The language and region of the device the pages are read on, as the browser says: `ko-KR`,
69
+ * `en-GB`. The fallback where there is no browser to ask. The week begins as this has it
70
+ * where the workspace leaves the choice to the system default (ADR-0035).
71
+ */
72
+ export function deviceLanguage(fallback: string): string {
73
+ const browser = (globalThis as { readonly navigator?: { readonly language?: string } }).navigator;
74
+ return browser?.language ?? fallback;
75
+ }
76
+
77
+ /**
78
+ * The weekday a week begins on: as the workspace says (ADR-0035), Monday or Sunday, else as the
79
+ * language and region have it, the device's by {@link deviceLanguage}. 1 for Monday, 7 for Sunday.
80
+ */
81
+ export function firstDayOfWeek(language: string, weekStart?: WeekStart | undefined): number {
82
+ if (weekStart === "monday") {
83
+ return 1;
84
+ }
85
+ if (weekStart === "sunday") {
86
+ return 7;
87
+ }
67
88
  try {
68
89
  const locale = new Intl.Locale(language) as Intl.Locale & {
69
90
  readonly weekInfo?: { readonly firstDay: number };
package/src/i18n/en.ts CHANGED
@@ -3,6 +3,8 @@ import type {
3
3
  CategoryColor,
4
4
  GoalState,
5
5
  MembershipSource,
6
+ PackConnectEnding,
7
+ PeopleVisibility,
6
8
  PersonRole,
7
9
  PersonStatus,
8
10
  ProjectRole,
@@ -12,6 +14,7 @@ import type {
12
14
  SkillsStatus,
13
15
  TaskPriority,
14
16
  TaskState,
17
+ WeekStart,
15
18
  } from "../vocabulary.js";
16
19
 
17
20
  // Every word the default console shows, in English: the source of the keys every other pack
@@ -44,9 +47,18 @@ export const en = {
44
47
  status: { active: "Active", deactivated: "Deactivated" } satisfies Record<PersonStatus, string>,
45
48
  projectRole: { owner: "Owner", member: "Member" } satisfies Record<ProjectRole, string>,
46
49
  visibility: {
47
- workspace: "Everyone of the workspace",
48
- private: "Only those listed",
50
+ workspace: "Open: everyone of the workspace",
51
+ private: "Private: only those listed",
49
52
  } satisfies Record<ProjectVisibility, string>,
53
+ peopleVisibility: {
54
+ everyone: "Everyone of the workspace",
55
+ shared_projects: "Only the people one shares a project with",
56
+ } satisfies Record<PeopleVisibility, string>,
57
+ weekStart: {
58
+ auto: "System default",
59
+ monday: "Monday",
60
+ sunday: "Sunday",
61
+ } satisfies Record<WeekStart, string>,
50
62
  runStatus: {
51
63
  running: "Open",
52
64
  waiting: "Waiting for a decision",
@@ -195,6 +207,13 @@ export const en = {
195
207
  chooseSomeone: "Choose someone",
196
208
  as: "As",
197
209
  add: "Add",
210
+ open: "An open project: everyone of the workspace is a member. The owners configure it, and an owner is made from this list.",
211
+ addByLoginAria: "Add a member by login",
212
+ login: "Login",
213
+ addByLogin: "Add by login",
214
+ lookupHint:
215
+ "Someone the list does not show: their login at the git host, or their address, exactly.",
216
+ nobodyCalled: (login: string) => `Nobody of the workspace is called ${login}.`,
198
217
  empty: {
199
218
  title: "Nobody is listed",
200
219
  workspace: "Everyone of the workspace is in; the administrators own it.",
@@ -202,11 +221,33 @@ export const en = {
202
221
  },
203
222
  },
204
223
  packToken: {
205
- aria: "The pack token",
224
+ aria: "The SkillCDN token",
225
+ intro:
226
+ "A private pack is read with a SkillCDN token for its repository; a public one needs none. Connect it with a code approved at SkillCDN, or paste a token made there by hand. The console keeps the token sealed and shows it to nobody. A project that reads the organization's pack may leave this to the workspace's token, on the Settings page.",
227
+ connectTitle: "With a code approved at SkillCDN",
228
+ pasteTitle: "With a token made by hand",
229
+ pasteAria: "Paste a token",
206
230
  label: "Token",
207
- hint: "A token a person made at SkillCDN for the pack's repository, on their account page there. The console keeps it sealed, reads the pack with it for whoever may see the project, and shows it to nobody.",
231
+ hint: "Made on the account page at SkillCDN, for the pack's repository.",
232
+ setSince: "A token has been set since",
208
233
  placeholder: "scdn_repo_…",
234
+ connect: "Connect at SkillCDN",
235
+ connectHint:
236
+ "The console asks SkillCDN for a code. Open the page it gives, signed in there as someone who may open the repository, check the code and approve.",
237
+ connectCode: "Code",
238
+ connectOpen: "Open the page at SkillCDN",
239
+ connectWaiting: "Waiting for the approval at SkillCDN.",
240
+ connectExpires: "The code expires",
241
+ connectCancel: "Stop waiting",
242
+ connectDone: "Connected: the console reads the pack with the token from now on.",
243
+ connectEnded: {
244
+ expired: "The code expired before anyone approved it. Connect again for a new one.",
245
+ declined: "The request was declined at SkillCDN, or the code was spent.",
246
+ refused:
247
+ "SkillCDN refused the request: the repository is not one the person may open, the pack is public and needs no token, or they hold as many tokens as one may.",
248
+ } satisfies Record<PackConnectEnding, string>,
209
249
  set: "Set the token",
250
+ replace: "Replace the token",
210
251
  remove: "Remove the token",
211
252
  },
212
253
  settings: {
@@ -215,10 +256,11 @@ export const en = {
215
256
  edit: "Edit the project",
216
257
  organizationsSkills: (address: string) => `The organization's: ${address}`,
217
258
  noSkills: "None: the console names no address for the organization's.",
218
- packToken: "Pack token",
219
- packTokenNone: "None: a public pack needs none, and a private one is not read.",
259
+ packToken: "SkillCDN token",
260
+ packTokenNone: "None",
220
261
  packTokenSet: "Set",
221
262
  packTokenEdit: "Change it",
263
+ packTokenAdd: "Set one",
222
264
  },
223
265
  },
224
266
 
@@ -498,6 +540,26 @@ export const en = {
498
540
  noSkillsAddress: "No address: each project names its own, or has none.",
499
541
  skillsFromConfiguration:
500
542
  "Set by whoever runs the console, in its configuration: the deployment the skills are read through, and the organization's address.",
543
+ skillsToken: "SkillCDN token",
544
+ skillsTokenIntro:
545
+ "Every project that names no address of its own, and holds no token of its own, reads the organization's pack with this token; a public pack needs none. Connect it with a code approved at SkillCDN, or paste a token made there by hand. The console keeps the token sealed and shows it to nobody.",
546
+ workspace: {
547
+ title: "Workspace",
548
+ aria: "Workspace settings",
549
+ peopleVisibility: "Member visibility",
550
+ peopleVisibilityHint:
551
+ "Whom a member sees on the People page and in the pickers: everyone of the workspace, or only the people they share a project with. An administrator always sees everyone, and a project open to the workspace has everyone in it.",
552
+ weekStart: "The week begins on",
553
+ weekStartHint:
554
+ "On the calendars and on My work. System default follows the language and region set on each person's device; Monday or Sunday holds for everyone.",
555
+ timeZone: "Time zone",
556
+ timeZoneHint:
557
+ "The console command and the agents print moments in this zone. The pages show them in each device's own. None: the command prints them in UTC.",
558
+ timeZoneNone: "None",
559
+ timeZoneDevice: (zone: string) => `This device: ${zone}`,
560
+ timeZoneUseDevice: (zone: string) => `Use this device's: ${zone}`,
561
+ save: "Save the settings",
562
+ },
501
563
  },
502
564
 
503
565
  rules: {
@@ -561,9 +623,14 @@ export const en = {
561
623
  dueOn: "the due date",
562
624
  state: "the state",
563
625
  color: "the colour",
626
+ peopleVisibility: "the member visibility",
627
+ weekStart: "the first day of the week",
628
+ timeZone: "the time zone",
629
+ packToken: "the SkillCDN token",
564
630
  } as Readonly<Record<string, string>>,
565
631
  event: {
566
632
  personJoined: () => "joined the board",
633
+ workspaceUpdated: (fields: string) => `changed ${fields} of the workspace`,
567
634
  roleChanged: (login: string, role: string) => `made ${login} ${role}`,
568
635
  personListed: (login: string) => `added ${login}`,
569
636
  personUnlisted: (login: string) => `took ${login} off the list`,
@@ -684,9 +751,9 @@ export const en = {
684
751
  indexing: "SkillCDN is still indexing the repository. Look again in a moment.",
685
752
  failed: "SkillCDN could not index the repository. What it found is on the address's page.",
686
753
  not_found:
687
- "SkillCDN does not serve this address to the console: there is nothing there, or the pack is private and the project holds no pack token for it (Settings).",
754
+ "SkillCDN does not serve this address to the console: there is nothing there, or the pack is private and no SkillCDN token reads it, the project's own or the workspace's. An owner connects one on the Settings page, with a code approved at SkillCDN.",
688
755
  token_invalid:
689
- "The pack token the project holds no longer works at SkillCDN: it expired, was removed, or its maker lost the repository. An owner sets a new one on the Settings page.",
756
+ "The SkillCDN token the pack is read with no longer works at SkillCDN: it expired, was removed, or its maker lost the repository. An owner sets a new one on the Settings page.",
690
757
  unavailable: "SkillCDN could not be reached. The skills are as they were; look again later.",
691
758
  },
692
759
  none: {
@@ -764,6 +831,7 @@ export const en = {
764
831
  people: {
765
832
  title: "People",
766
833
  list: "People",
834
+ narrowed: "The people you share a project with, and the administrators.",
767
835
  roleOf: (name: string) => `Role of ${name}`,
768
836
  letIn: "Add a person",
769
837
  letInAria: "Add a person",