@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.
Files changed (156) hide show
  1. package/README.md +8 -7
  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 +248 -92
  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 +13 -28
  19. package/dist/cli/format.d.ts.map +1 -1
  20. package/dist/cli/format.js +74 -274
  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 +36 -19
  24. package/dist/cli/help.js.map +1 -1
  25. package/dist/cli/pack.d.ts +46 -0
  26. package/dist/cli/pack.d.ts.map +1 -0
  27. package/dist/cli/pack.js +250 -0
  28. package/dist/cli/pack.js.map +1 -0
  29. package/dist/client.d.ts +26 -1
  30. package/dist/client.d.ts.map +1 -1
  31. package/dist/client.js +14 -1
  32. package/dist/client.js.map +1 -1
  33. package/dist/compat.d.ts +1 -1
  34. package/dist/compat.js +1 -1
  35. package/dist/components/calendar.d.ts +5 -1
  36. package/dist/components/calendar.d.ts.map +1 -1
  37. package/dist/components/calendar.js +3 -3
  38. package/dist/components/calendar.js.map +1 -1
  39. package/dist/components/decision-list.d.ts +13 -1
  40. package/dist/components/decision-list.d.ts.map +1 -1
  41. package/dist/components/decision-list.js +74 -29
  42. package/dist/components/decision-list.js.map +1 -1
  43. package/dist/components/event-feed.d.ts.map +1 -1
  44. package/dist/components/event-feed.js +2 -0
  45. package/dist/components/event-feed.js.map +1 -1
  46. package/dist/components/pack-connect.d.ts +23 -0
  47. package/dist/components/pack-connect.d.ts.map +1 -0
  48. package/dist/components/pack-connect.js +86 -0
  49. package/dist/components/pack-connect.js.map +1 -0
  50. package/dist/components/pack-token.d.ts +27 -0
  51. package/dist/components/pack-token.d.ts.map +1 -0
  52. package/dist/components/pack-token.js +11 -0
  53. package/dist/components/pack-token.js.map +1 -0
  54. package/dist/components/paged.d.ts +24 -0
  55. package/dist/components/paged.d.ts.map +1 -1
  56. package/dist/components/paged.js +40 -4
  57. package/dist/components/paged.js.map +1 -1
  58. package/dist/components/projects.d.ts +13 -0
  59. package/dist/components/projects.d.ts.map +1 -1
  60. package/dist/components/projects.js +37 -5
  61. package/dist/components/projects.js.map +1 -1
  62. package/dist/components/skills.d.ts.map +1 -1
  63. package/dist/components/skills.js +8 -4
  64. package/dist/components/skills.js.map +1 -1
  65. package/dist/components/ui.d.ts +2 -0
  66. package/dist/components/ui.d.ts.map +1 -1
  67. package/dist/components/ui.js +7 -2
  68. package/dist/components/ui.js.map +1 -1
  69. package/dist/components/work.d.ts +3 -1
  70. package/dist/components/work.d.ts.map +1 -1
  71. package/dist/components/work.js +1 -1
  72. package/dist/components/work.js.map +1 -1
  73. package/dist/components/workspace.d.ts +46 -0
  74. package/dist/components/workspace.d.ts.map +1 -0
  75. package/dist/components/workspace.js +124 -0
  76. package/dist/components/workspace.js.map +1 -0
  77. package/dist/console.css +268 -5
  78. package/dist/console.d.ts.map +1 -1
  79. package/dist/console.js +62 -23
  80. package/dist/console.js.map +1 -1
  81. package/dist/data.d.ts +16 -1
  82. package/dist/data.d.ts.map +1 -1
  83. package/dist/data.js +64 -2
  84. package/dist/data.js.map +1 -1
  85. package/dist/dates.d.ts +12 -2
  86. package/dist/dates.d.ts.map +1 -1
  87. package/dist/dates.js +20 -2
  88. package/dist/dates.js.map +1 -1
  89. package/dist/i18n/en.d.ts +66 -0
  90. package/dist/i18n/en.d.ts.map +1 -1
  91. package/dist/i18n/en.js +80 -8
  92. package/dist/i18n/en.js.map +1 -1
  93. package/dist/i18n/ko.d.ts.map +1 -1
  94. package/dist/i18n/ko.js +75 -7
  95. package/dist/i18n/ko.js.map +1 -1
  96. package/dist/index.d.ts +5 -2
  97. package/dist/index.d.ts.map +1 -1
  98. package/dist/index.js +5 -2
  99. package/dist/index.js.map +1 -1
  100. package/dist/router.d.ts +10 -5
  101. package/dist/router.d.ts.map +1 -1
  102. package/dist/router.js +24 -9
  103. package/dist/router.js.map +1 -1
  104. package/dist/routes.d.ts +4 -0
  105. package/dist/routes.d.ts.map +1 -1
  106. package/dist/routes.js +4 -0
  107. package/dist/routes.js.map +1 -1
  108. package/dist/schemas.d.ts +500 -158
  109. package/dist/schemas.d.ts.map +1 -1
  110. package/dist/schemas.js +169 -165
  111. package/dist/schemas.js.map +1 -1
  112. package/dist/vocabulary.d.ts +19 -1
  113. package/dist/vocabulary.d.ts.map +1 -1
  114. package/dist/vocabulary.js +16 -0
  115. package/dist/vocabulary.js.map +1 -1
  116. package/docs/specs/cli.md +19 -14
  117. package/docs/specs/rest.md +34 -19
  118. package/package.json +2 -1
  119. package/skills/building-a-console/SKILL.md +5 -5
  120. package/skills/building-a-console/references/the-look-from-a-website.md +39 -0
  121. package/skills/deploying-a-console/SKILL.md +9 -8
  122. package/skills/working-the-board/SKILL.md +5 -5
  123. package/skills/working-the-board/references/setting-up.md +1 -1
  124. package/skills/working-the-board/references/terms.md +2 -0
  125. package/skills/working-the-board/references/the-sentence.md +1 -1
  126. package/src/api.ts +18 -7
  127. package/src/cli/cli.ts +345 -109
  128. package/src/cli/credentials.ts +7 -0
  129. package/src/cli/directory.ts +9 -0
  130. package/src/cli/format.ts +82 -341
  131. package/src/cli/help.ts +43 -19
  132. package/src/cli/pack.ts +356 -0
  133. package/src/client.ts +84 -0
  134. package/src/compat.ts +1 -1
  135. package/src/components/calendar.tsx +8 -2
  136. package/src/components/decision-list.tsx +227 -94
  137. package/src/components/event-feed.tsx +2 -0
  138. package/src/components/pack-connect.tsx +179 -0
  139. package/src/components/pack-token.tsx +74 -0
  140. package/src/components/paged.tsx +72 -3
  141. package/src/components/projects.tsx +108 -9
  142. package/src/components/skills.tsx +51 -33
  143. package/src/components/ui.tsx +16 -3
  144. package/src/components/work.tsx +4 -1
  145. package/src/components/workspace.tsx +298 -0
  146. package/src/console.tsx +176 -29
  147. package/src/data.ts +89 -2
  148. package/src/dates.ts +23 -2
  149. package/src/i18n/en.ts +94 -8
  150. package/src/i18n/ko.ts +84 -7
  151. package/src/index.ts +25 -1
  152. package/src/router.ts +37 -14
  153. package/src/routes.ts +4 -0
  154. package/src/schemas.ts +202 -189
  155. package/src/styles/console.css +268 -5
  156. 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";
@@ -98,6 +105,7 @@ import {
98
105
  calendarHref,
99
106
  customPageHref,
100
107
  decisionHref,
108
+ decisionsHref,
101
109
  docHref,
102
110
  type GoalTaskView,
103
111
  goalHref,
@@ -860,6 +868,7 @@ function ProjectPage(props: {
860
868
  tasks={shown}
861
869
  month={month}
862
870
  today={now}
871
+ weekStart={weekStartOf(data.me?.workspace)}
863
872
  categories={data.categories}
864
873
  goals={data.goals}
865
874
  taskHref={href}
@@ -1074,6 +1083,7 @@ function ProjectPage(props: {
1074
1083
  tasks={towards}
1075
1084
  month={month}
1076
1085
  today={now}
1086
+ weekStart={weekStartOf(data.me?.workspace)}
1077
1087
  categories={data.categories}
1078
1088
  goals={[goal]}
1079
1089
  taskHref={href}
@@ -1112,6 +1122,8 @@ function ProjectPage(props: {
1112
1122
  taskHref={href}
1113
1123
  docHref={toDoc}
1114
1124
  decisionHref={(decision) => decisionHref(key, decision.id)}
1125
+ page={route.page}
1126
+ pageHref={(page) => decisionsHref(key, page)}
1115
1127
  onAnswer={onAnswer}
1116
1128
  onUpdate={onUpdate}
1117
1129
  busy={busy}
@@ -1217,19 +1229,34 @@ function ProjectPage(props: {
1217
1229
  const me = data.me?.person ?? undefined;
1218
1230
  const configures = owner;
1219
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";
1220
1239
  return (
1221
1240
  <>
1222
1241
  <div className="sc-page-head">
1223
1242
  <h1 className="sc-page-title">{members.title}</h1>
1224
1243
  </div>
1225
- <p className="sc-lead">{words.vocabulary.visibility[project.visibility]}</p>
1244
+ {open && <p className="sc-lead">{members.open}</p>}
1226
1245
  {error !== undefined && <Callout tone="danger">{error}</Callout>}
1227
1246
  <components.MemberList
1228
1247
  members={data.members}
1229
1248
  people={data.people.filter(inWorkspace)}
1230
1249
  me={me}
1250
+ open={open}
1231
1251
  busy={busy}
1232
- 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
+ }
1233
1260
  onChangeRole={
1234
1261
  configures
1235
1262
  ? (member, role) =>
@@ -1237,7 +1264,7 @@ function ProjectPage(props: {
1237
1264
  : undefined
1238
1265
  }
1239
1266
  onRemove={
1240
- configures
1267
+ configures && !open
1241
1268
  ? (member) => void act(() => data.actions.removeMember(member.person.id))
1242
1269
  : undefined
1243
1270
  }
@@ -1379,28 +1406,33 @@ function ProjectSettingsPage(props: {
1379
1406
  <>
1380
1407
  {head}
1381
1408
  {error !== undefined && <Callout tone="danger">{error}</Callout>}
1382
- <section className="sc-panel sc-panel-form" aria-label={words.projects.packToken.aria}>
1383
- <PackTokenForm
1384
- set={project.packToken !== null}
1385
- busy={busy}
1386
- onSubmit={(token) =>
1387
- void act(async () => {
1388
- await data.actions.setPackToken(token);
1389
- navigation.replace(here);
1390
- })
1391
- }
1392
- onRemove={
1393
- project.packToken === null
1394
- ? undefined
1395
- : () =>
1396
- void act(async () => {
1397
- await data.actions.removePackToken();
1398
- navigation.replace(here);
1399
- })
1400
- }
1401
- onCancel={() => navigation.back(here)}
1402
- />
1403
- </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
+ />
1404
1436
  </>
1405
1437
  );
1406
1438
  }
@@ -1452,7 +1484,9 @@ function ProjectSettingsPage(props: {
1452
1484
  {owner && (
1453
1485
  <>
1454
1486
  {" "}
1455
- <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>
1456
1490
  </>
1457
1491
  )}
1458
1492
  </dd>
@@ -1513,7 +1547,7 @@ function ProjectSettingsPage(props: {
1513
1547
  * from, which whoever runs the console set. Read by everyone; the rules written by an administrator.
1514
1548
  */
1515
1549
  function WorkspaceSettingsPage(props: {
1516
- readonly form: "rules" | undefined;
1550
+ readonly form: "rules" | "workspace" | "pack-token" | undefined;
1517
1551
  readonly data: ConsoleData;
1518
1552
  readonly navigation: Navigation;
1519
1553
  }) {
@@ -1521,7 +1555,8 @@ function WorkspaceSettingsPage(props: {
1521
1555
  const { form, data, navigation } = props;
1522
1556
  const { busy, error, act } = useAction();
1523
1557
  const administrator = data.me?.person?.role === "admin";
1524
- const skills = data.me?.workspace.skills;
1558
+ const workspace = data.me?.workspace;
1559
+ const skills = workspace?.skills;
1525
1560
  const [rules, setRules] = useState<RestRules | undefined>(undefined);
1526
1561
  useEffect(() => {
1527
1562
  const controller = new AbortController();
@@ -1584,10 +1619,90 @@ function WorkspaceSettingsPage(props: {
1584
1619
  </>
1585
1620
  );
1586
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
+ }
1587
1686
  return (
1588
1687
  <>
1589
1688
  {head}
1590
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
+ )}
1591
1706
  <section className="sc-panel" aria-label={words.rules.title}>
1592
1707
  <h2 className="sc-section-title">{words.rules.title}</h2>
1593
1708
  {rules === undefined ? (
@@ -1648,6 +1763,30 @@ function WorkspaceSettingsPage(props: {
1648
1763
  )}
1649
1764
  </dd>
1650
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
+ )}
1651
1790
  </dl>
1652
1791
  <p className="sc-muted">{words.settings.skillsFromConfiguration}</p>
1653
1792
  </section>
@@ -1774,6 +1913,8 @@ function PlatePage(props: {
1774
1913
  readonly empty: { readonly title: string; readonly body: string };
1775
1914
  /** What a refusal says where the plate may not be the asker's to see; the server's words otherwise. */
1776
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;
1777
1918
  }) {
1778
1919
  const words = useWords();
1779
1920
  const { components, load, couldNotLoad, refused } = props;
@@ -1834,6 +1975,7 @@ function PlatePage(props: {
1834
1975
  <components.WorkList
1835
1976
  work={plate}
1836
1977
  week={props.week}
1978
+ weekStart={props.weekStart}
1837
1979
  taskHref={(projectKey, task) => taskHref(projectKey, task.number)}
1838
1980
  decisionHref={(projectKey, decisionId) => decisionHref(projectKey, decisionId)}
1839
1981
  weekHref={props.weekHref}
@@ -1857,6 +1999,7 @@ function WorkPage(props: {
1857
1999
  load={work}
1858
2000
  title={words.work.title}
1859
2001
  week={props.week}
2002
+ weekStart={weekStartOf(props.data.me?.workspace)}
1860
2003
  weekHref={workHref}
1861
2004
  loading={words.work.loading}
1862
2005
  couldNotLoad={words.work.couldNotLoad}
@@ -1885,6 +2028,7 @@ function PersonWorkPage(props: {
1885
2028
  title={words.people.workOf(name)}
1886
2029
  aside={<LinkButton href={PATHS.people}>{words.nav.people}</LinkButton>}
1887
2030
  week={props.week}
2031
+ weekStart={weekStartOf(data.me?.workspace)}
1888
2032
  weekHref={(day) => personWorkHref(id, day)}
1889
2033
  loading={words.people.loadingWork}
1890
2034
  couldNotLoad={words.people.couldNotLoadWork}
@@ -1955,6 +2099,8 @@ function WorkspacePage(props: {
1955
2099
  const making = route.name === "new-listing" && administrator;
1956
2100
  // An owner sees the work of the people in their projects; the console refuses the rest.
1957
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;
1958
2104
  return (
1959
2105
  <>
1960
2106
  <div className="sc-page-head">
@@ -1965,6 +2111,7 @@ function WorkspacePage(props: {
1965
2111
  </LinkButton>
1966
2112
  )}
1967
2113
  </div>
2114
+ {narrowed && <p className="sc-lead">{words.people.narrowed}</p>}
1968
2115
  {error !== undefined && <Callout tone="danger">{error}</Callout>}
1969
2116
  {making && (
1970
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. */
@@ -245,6 +265,28 @@ async function readTasks(
245
265
  return { items };
246
266
  }
247
267
 
268
+ /** How many pages of the decisions are read before the record is shown as it stands. */
269
+ const DECISIONS_MAX_PAGES = 20;
270
+
271
+ /**
272
+ * The decisions of a project, whole: page after page while the server says there is more, so
273
+ * that the record is read to its end and the pages turn it without asking again.
274
+ */
275
+ async function readDecisions(
276
+ scope: ProjectClient,
277
+ signal: AbortSignal | undefined,
278
+ ): Promise<{ readonly items: RestDecision[] }> {
279
+ const items: RestDecision[] = [];
280
+ for (let page = 0; page < DECISIONS_MAX_PAGES; page += 1) {
281
+ const result = await scope.decisions({ limit: LIST_LIMIT, offset: page * LIST_LIMIT }, signal);
282
+ items.push(...result.items);
283
+ if (!result.more) {
284
+ break;
285
+ }
286
+ }
287
+ return { items };
288
+ }
289
+
248
290
  const wordsOf = (failure: unknown, fallback: string): string =>
249
291
  failure instanceof ApiError ? failure.message : fallback;
250
292
 
@@ -310,7 +352,7 @@ export function useConsoleData(client: ConsoleClient, projectKey: string | undef
310
352
  await Promise.all([
311
353
  scope.get(signal),
312
354
  readTasks(scope, signal),
313
- scope.decisions(undefined, signal),
355
+ readDecisions(scope, signal),
314
356
  scope.goals({}, signal),
315
357
  scope.runs(undefined, signal),
316
358
  scope.members(signal),
@@ -427,7 +469,15 @@ export function useConsoleData(client: ConsoleClient, projectKey: string | undef
427
469
  // No answer at all is said on the page as the deployment being out of reach,
428
470
  // rather than waited for.
429
471
  if (!signal.aborted) {
430
- setSkills({ address: null, source: "", page: null, status: "unavailable", items: [] });
472
+ setSkills({
473
+ address: null,
474
+ source: "",
475
+ page: null,
476
+ status: "unavailable",
477
+ refusal: null,
478
+ items: [],
479
+ mount: null,
480
+ });
431
481
  }
432
482
  });
433
483
  if (typeof EventSource === "undefined") {
@@ -508,6 +558,15 @@ export function useConsoleData(client: ConsoleClient, projectKey: string | undef
508
558
  setProject(changed);
509
559
  return changed;
510
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(),
511
570
  async addMember(input) {
512
571
  const member = await scope().addMember(input);
513
572
  await refreshProject();
@@ -619,6 +678,34 @@ export function useConsoleData(client: ConsoleClient, projectKey: string | undef
619
678
  setMe(answer);
620
679
  return answer;
621
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),
622
709
  async abandonRun(id) {
623
710
  const run = await scope().endRun(id, { status: "abandoned" });
624
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 };