@kelpie/ui 0.12.0 → 0.14.0

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 (300) hide show
  1. package/dist/api/resources/attendances.d.ts +13 -0
  2. package/dist/api/resources/attendances.d.ts.map +1 -0
  3. package/dist/api/resources/attendances.js +34 -0
  4. package/dist/api/resources/attendances.js.map +1 -0
  5. package/dist/api/resources/eventAssociations.d.ts +12 -0
  6. package/dist/api/resources/eventAssociations.d.ts.map +1 -0
  7. package/dist/api/resources/eventAssociations.js +25 -0
  8. package/dist/api/resources/eventAssociations.js.map +1 -0
  9. package/dist/api/resources/events.d.ts +17 -0
  10. package/dist/api/resources/events.d.ts.map +1 -0
  11. package/dist/api/resources/events.js +40 -0
  12. package/dist/api/resources/events.js.map +1 -0
  13. package/dist/api/resources/handbookSeed.d.ts +11 -0
  14. package/dist/api/resources/handbookSeed.d.ts.map +1 -0
  15. package/dist/api/resources/handbookSeed.js +43 -0
  16. package/dist/api/resources/handbookSeed.js.map +1 -0
  17. package/dist/api/resources/moduleSettings.d.ts +10 -0
  18. package/dist/api/resources/moduleSettings.d.ts.map +1 -1
  19. package/dist/api/resources/moduleSettings.js +21 -0
  20. package/dist/api/resources/moduleSettings.js.map +1 -1
  21. package/dist/api/resources/planItems.d.ts +4 -4
  22. package/dist/api/resources/planItems.d.ts.map +1 -1
  23. package/dist/api/resources/planItems.js +1 -1
  24. package/dist/api/resources/planItems.js.map +1 -1
  25. package/dist/api/resources/publicConfig.js +3 -3
  26. package/dist/api/resources/sampleData.d.ts.map +1 -1
  27. package/dist/api/resources/sampleData.js +2 -0
  28. package/dist/api/resources/sampleData.js.map +1 -1
  29. package/dist/api/resources/session.d.ts +22 -1
  30. package/dist/api/resources/session.d.ts.map +1 -1
  31. package/dist/api/resources/session.js +43 -1
  32. package/dist/api/resources/session.js.map +1 -1
  33. package/dist/api/resources/workspace.d.ts.map +1 -1
  34. package/dist/api/resources/workspace.js +2 -3
  35. package/dist/api/resources/workspace.js.map +1 -1
  36. package/dist/app/KelpieApp.d.ts.map +1 -1
  37. package/dist/app/KelpieApp.js +5 -1
  38. package/dist/app/KelpieApp.js.map +1 -1
  39. package/dist/components/ActivitiesPanel.d.ts.map +1 -1
  40. package/dist/components/ActivitiesPanel.js +2 -0
  41. package/dist/components/ActivitiesPanel.js.map +1 -1
  42. package/dist/components/AddressesField.d.ts +23 -0
  43. package/dist/components/AddressesField.d.ts.map +1 -0
  44. package/dist/components/AddressesField.js +215 -0
  45. package/dist/components/AddressesField.js.map +1 -0
  46. package/dist/components/CountrySearch.d.ts +16 -0
  47. package/dist/components/CountrySearch.d.ts.map +1 -0
  48. package/dist/components/CountrySearch.js +30 -0
  49. package/dist/components/CountrySearch.js.map +1 -0
  50. package/dist/components/EnvironmentBanner.d.ts +0 -12
  51. package/dist/components/EnvironmentBanner.d.ts.map +1 -1
  52. package/dist/components/EnvironmentBanner.js +21 -6
  53. package/dist/components/EnvironmentBanner.js.map +1 -1
  54. package/dist/components/InlineEdit.d.ts +7 -1
  55. package/dist/components/InlineEdit.d.ts.map +1 -1
  56. package/dist/components/InlineEdit.js +8 -1
  57. package/dist/components/InlineEdit.js.map +1 -1
  58. package/dist/components/LinkedEventsPanel.d.ts +11 -0
  59. package/dist/components/LinkedEventsPanel.d.ts.map +1 -0
  60. package/dist/components/LinkedEventsPanel.js +18 -0
  61. package/dist/components/LinkedEventsPanel.js.map +1 -0
  62. package/dist/components/NotesPanel.d.ts +4 -6
  63. package/dist/components/NotesPanel.d.ts.map +1 -1
  64. package/dist/components/NotesPanel.js +39 -3
  65. package/dist/components/NotesPanel.js.map +1 -1
  66. package/dist/components/PlanAttention.d.ts.map +1 -1
  67. package/dist/components/PlanAttention.js +3 -2
  68. package/dist/components/PlanAttention.js.map +1 -1
  69. package/dist/components/PlanPanel.d.ts +3 -3
  70. package/dist/components/PlanPanel.d.ts.map +1 -1
  71. package/dist/components/Shell.d.ts.map +1 -1
  72. package/dist/components/Shell.js +8 -2
  73. package/dist/components/Shell.js.map +1 -1
  74. package/dist/components/SocialNetworkIcon.d.ts +17 -0
  75. package/dist/components/SocialNetworkIcon.d.ts.map +1 -0
  76. package/dist/components/SocialNetworkIcon.js +39 -0
  77. package/dist/components/SocialNetworkIcon.js.map +1 -0
  78. package/dist/components/SocialProfilesField.d.ts +8 -0
  79. package/dist/components/SocialProfilesField.d.ts.map +1 -1
  80. package/dist/components/SocialProfilesField.js +19 -5
  81. package/dist/components/SocialProfilesField.js.map +1 -1
  82. package/dist/components/TimezoneSearch.d.ts +21 -0
  83. package/dist/components/TimezoneSearch.d.ts.map +1 -0
  84. package/dist/components/TimezoneSearch.js +27 -0
  85. package/dist/components/TimezoneSearch.js.map +1 -0
  86. package/dist/components/WorkspaceSwitcher.d.ts +8 -0
  87. package/dist/components/WorkspaceSwitcher.d.ts.map +1 -0
  88. package/dist/components/WorkspaceSwitcher.js +67 -0
  89. package/dist/components/WorkspaceSwitcher.js.map +1 -0
  90. package/dist/components/environmentTone.d.ts +10 -0
  91. package/dist/components/environmentTone.d.ts.map +1 -0
  92. package/dist/components/environmentTone.js +35 -0
  93. package/dist/components/environmentTone.js.map +1 -0
  94. package/dist/index.d.ts +21 -4
  95. package/dist/index.d.ts.map +1 -1
  96. package/dist/index.js +12 -2
  97. package/dist/index.js.map +1 -1
  98. package/dist/lib/timezones.d.ts +17 -0
  99. package/dist/lib/timezones.d.ts.map +1 -0
  100. package/dist/lib/timezones.js +39 -0
  101. package/dist/lib/timezones.js.map +1 -0
  102. package/dist/pages/CompaniesPage.d.ts.map +1 -1
  103. package/dist/pages/CompaniesPage.js +12 -11
  104. package/dist/pages/CompaniesPage.js.map +1 -1
  105. package/dist/pages/CompanyDetail.d.ts.map +1 -1
  106. package/dist/pages/CompanyDetail.js +23 -12
  107. package/dist/pages/CompanyDetail.js.map +1 -1
  108. package/dist/pages/DealDetail.d.ts.map +1 -1
  109. package/dist/pages/DealDetail.js +5 -1
  110. package/dist/pages/DealDetail.js.map +1 -1
  111. package/dist/pages/DecisionsPage.d.ts.map +1 -1
  112. package/dist/pages/DecisionsPage.js +7 -0
  113. package/dist/pages/DecisionsPage.js.map +1 -1
  114. package/dist/pages/EnquiryDetail.d.ts.map +1 -1
  115. package/dist/pages/EnquiryDetail.js +5 -1
  116. package/dist/pages/EnquiryDetail.js.map +1 -1
  117. package/dist/pages/EventDetail.d.ts +2 -0
  118. package/dist/pages/EventDetail.d.ts.map +1 -0
  119. package/dist/pages/EventDetail.js +332 -0
  120. package/dist/pages/EventDetail.js.map +1 -0
  121. package/dist/pages/EventsPage.d.ts +2 -0
  122. package/dist/pages/EventsPage.d.ts.map +1 -0
  123. package/dist/pages/EventsPage.js +176 -0
  124. package/dist/pages/EventsPage.js.map +1 -0
  125. package/dist/pages/ListDetail.d.ts.map +1 -1
  126. package/dist/pages/ListDetail.js +17 -0
  127. package/dist/pages/ListDetail.js.map +1 -1
  128. package/dist/pages/OpportunityDetail.d.ts.map +1 -1
  129. package/dist/pages/OpportunityDetail.js +5 -1
  130. package/dist/pages/OpportunityDetail.js.map +1 -1
  131. package/dist/pages/PartnershipDetail.d.ts.map +1 -1
  132. package/dist/pages/PartnershipDetail.js +5 -1
  133. package/dist/pages/PartnershipDetail.js.map +1 -1
  134. package/dist/pages/PeoplePage.d.ts.map +1 -1
  135. package/dist/pages/PeoplePage.js +13 -5
  136. package/dist/pages/PeoplePage.js.map +1 -1
  137. package/dist/pages/PersonDetail.d.ts.map +1 -1
  138. package/dist/pages/PersonDetail.js +94 -28
  139. package/dist/pages/PersonDetail.js.map +1 -1
  140. package/dist/pages/PlanningPage.d.ts.map +1 -1
  141. package/dist/pages/PlanningPage.js +6 -3
  142. package/dist/pages/PlanningPage.js.map +1 -1
  143. package/dist/pages/RaiseDetail.d.ts.map +1 -1
  144. package/dist/pages/RaiseDetail.js +5 -1
  145. package/dist/pages/RaiseDetail.js.map +1 -1
  146. package/dist/pages/RoleDetail.d.ts.map +1 -1
  147. package/dist/pages/RoleDetail.js +4 -1
  148. package/dist/pages/RoleDetail.js.map +1 -1
  149. package/dist/pages/SearchPage.d.ts.map +1 -1
  150. package/dist/pages/SearchPage.js +2 -1
  151. package/dist/pages/SearchPage.js.map +1 -1
  152. package/dist/pages/account/PreferencesPage.d.ts.map +1 -1
  153. package/dist/pages/account/PreferencesPage.js +2 -28
  154. package/dist/pages/account/PreferencesPage.js.map +1 -1
  155. package/dist/pages/admin/DataPage.js +10 -9
  156. package/dist/pages/admin/DataPage.js.map +1 -1
  157. package/dist/pages/admin/FieldsPage.js +1 -1
  158. package/dist/pages/admin/FieldsPage.js.map +1 -1
  159. package/dist/pages/admin/ModulesPage.d.ts.map +1 -1
  160. package/dist/pages/admin/ModulesPage.js +1 -0
  161. package/dist/pages/admin/ModulesPage.js.map +1 -1
  162. package/dist/pages/admin/WorkspaceSettingsPage.d.ts +12 -0
  163. package/dist/pages/admin/WorkspaceSettingsPage.d.ts.map +1 -1
  164. package/dist/pages/admin/WorkspaceSettingsPage.js +2 -21
  165. package/dist/pages/admin/WorkspaceSettingsPage.js.map +1 -1
  166. package/dist/pages/auth/AuthLayout.d.ts +5 -4
  167. package/dist/pages/auth/AuthLayout.d.ts.map +1 -1
  168. package/dist/pages/auth/AuthLayout.js +7 -5
  169. package/dist/pages/auth/AuthLayout.js.map +1 -1
  170. package/dist/pages/auth/RegionSwitcher.d.ts +10 -0
  171. package/dist/pages/auth/RegionSwitcher.d.ts.map +1 -0
  172. package/dist/pages/auth/RegionSwitcher.js +28 -0
  173. package/dist/pages/auth/RegionSwitcher.js.map +1 -0
  174. package/dist/pages/auth/SignInPage.d.ts.map +1 -1
  175. package/dist/pages/auth/SignInPage.js +3 -1
  176. package/dist/pages/auth/SignInPage.js.map +1 -1
  177. package/dist/pages/auth/SignUpPage.d.ts.map +1 -1
  178. package/dist/pages/auth/SignUpPage.js +7 -0
  179. package/dist/pages/auth/SignUpPage.js.map +1 -1
  180. package/dist/pages/auth/regionHref.d.ts +6 -0
  181. package/dist/pages/auth/regionHref.d.ts.map +1 -0
  182. package/dist/pages/auth/regionHref.js +8 -0
  183. package/dist/pages/auth/regionHref.js.map +1 -0
  184. package/dist/pages/dashboard/DashboardPage.d.ts +8 -0
  185. package/dist/pages/dashboard/DashboardPage.d.ts.map +1 -1
  186. package/dist/pages/dashboard/DashboardPage.js +5 -13
  187. package/dist/pages/dashboard/DashboardPage.js.map +1 -1
  188. package/dist/pages/dashboard/attention.d.ts +1 -8
  189. package/dist/pages/dashboard/attention.d.ts.map +1 -1
  190. package/dist/pages/dashboard/attention.js +21 -54
  191. package/dist/pages/dashboard/attention.js.map +1 -1
  192. package/dist/pages/dashboard/sections.d.ts.map +1 -1
  193. package/dist/pages/dashboard/sections.js +3 -2
  194. package/dist/pages/dashboard/sections.js.map +1 -1
  195. package/dist/pages/forms/FormActions.d.ts.map +1 -1
  196. package/dist/pages/forms/FormActions.js +7 -3
  197. package/dist/pages/forms/FormActions.js.map +1 -1
  198. package/dist/pages/onboarding/HandbookStepPage.d.ts +4 -8
  199. package/dist/pages/onboarding/HandbookStepPage.d.ts.map +1 -1
  200. package/dist/pages/onboarding/HandbookStepPage.js +13 -13
  201. package/dist/pages/onboarding/HandbookStepPage.js.map +1 -1
  202. package/dist/pages/onboarding/InvitesStepPage.d.ts.map +1 -1
  203. package/dist/pages/onboarding/InvitesStepPage.js +13 -7
  204. package/dist/pages/onboarding/InvitesStepPage.js.map +1 -1
  205. package/dist/pages/onboarding/ModulesStepPage.d.ts +11 -0
  206. package/dist/pages/onboarding/ModulesStepPage.d.ts.map +1 -0
  207. package/dist/pages/onboarding/ModulesStepPage.js +80 -0
  208. package/dist/pages/onboarding/ModulesStepPage.js.map +1 -0
  209. package/dist/pages/onboarding/OnboardingNav.d.ts +17 -0
  210. package/dist/pages/onboarding/OnboardingNav.d.ts.map +1 -0
  211. package/dist/pages/onboarding/OnboardingNav.js +37 -0
  212. package/dist/pages/onboarding/OnboardingNav.js.map +1 -0
  213. package/dist/pages/onboarding/OrganisationStepPage.d.ts +13 -0
  214. package/dist/pages/onboarding/OrganisationStepPage.d.ts.map +1 -0
  215. package/dist/pages/onboarding/OrganisationStepPage.js +59 -0
  216. package/dist/pages/onboarding/OrganisationStepPage.js.map +1 -0
  217. package/dist/pages/onboarding/WorkspaceStepPage.d.ts.map +1 -1
  218. package/dist/pages/onboarding/WorkspaceStepPage.js +25 -20
  219. package/dist/pages/onboarding/WorkspaceStepPage.js.map +1 -1
  220. package/dist/pages/onboarding/handbookTemplates.d.ts +19 -0
  221. package/dist/pages/onboarding/handbookTemplates.d.ts.map +1 -0
  222. package/dist/pages/onboarding/handbookTemplates.js +57 -0
  223. package/dist/pages/onboarding/handbookTemplates.js.map +1 -0
  224. package/dist/pages/onboarding/onboardingModules.d.ts +23 -0
  225. package/dist/pages/onboarding/onboardingModules.d.ts.map +1 -0
  226. package/dist/pages/onboarding/onboardingModules.js +153 -0
  227. package/dist/pages/onboarding/onboardingModules.js.map +1 -0
  228. package/dist/pages/onboarding/onboardingRerun.d.ts +21 -0
  229. package/dist/pages/onboarding/onboardingRerun.d.ts.map +1 -0
  230. package/dist/pages/onboarding/onboardingRerun.js +33 -0
  231. package/dist/pages/onboarding/onboardingRerun.js.map +1 -0
  232. package/package.json +4 -3
  233. package/src/api/resources/attendances.ts +54 -0
  234. package/src/api/resources/eventAssociations.ts +43 -0
  235. package/src/api/resources/events.ts +70 -0
  236. package/src/api/resources/handbookSeed.ts +63 -0
  237. package/src/api/resources/moduleSettings.ts +25 -0
  238. package/src/api/resources/planItems.ts +5 -5
  239. package/src/api/resources/publicConfig.ts +3 -3
  240. package/src/api/resources/sampleData.ts +2 -0
  241. package/src/api/resources/session.ts +59 -0
  242. package/src/api/resources/workspace.ts +2 -3
  243. package/src/app/KelpieApp.tsx +11 -3
  244. package/src/components/ActivitiesPanel.tsx +2 -0
  245. package/src/components/AddressesField.tsx +502 -0
  246. package/src/components/CountrySearch.tsx +71 -0
  247. package/src/components/EnvironmentBanner.tsx +23 -6
  248. package/src/components/InlineEdit.tsx +35 -1
  249. package/src/components/LinkedEventsPanel.tsx +50 -0
  250. package/src/components/NotesPanel.tsx +132 -15
  251. package/src/components/PlanAttention.tsx +5 -4
  252. package/src/components/PlanPanel.tsx +3 -3
  253. package/src/components/Shell.tsx +16 -1
  254. package/src/components/SocialNetworkIcon.tsx +88 -0
  255. package/src/components/SocialProfilesField.tsx +57 -10
  256. package/src/components/TimezoneSearch.tsx +67 -0
  257. package/src/components/WorkspaceSwitcher.tsx +136 -0
  258. package/src/components/environmentTone.ts +40 -0
  259. package/src/index.ts +42 -3
  260. package/src/lib/timezones.ts +43 -0
  261. package/src/pages/CompaniesPage.tsx +14 -13
  262. package/src/pages/CompanyDetail.tsx +59 -27
  263. package/src/pages/DealDetail.tsx +5 -0
  264. package/src/pages/DecisionsPage.tsx +7 -0
  265. package/src/pages/EnquiryDetail.tsx +7 -0
  266. package/src/pages/EventDetail.tsx +794 -0
  267. package/src/pages/EventsPage.tsx +382 -0
  268. package/src/pages/ListDetail.tsx +24 -0
  269. package/src/pages/OpportunityDetail.tsx +7 -0
  270. package/src/pages/PartnershipDetail.tsx +7 -0
  271. package/src/pages/PeoplePage.tsx +18 -5
  272. package/src/pages/PersonDetail.tsx +300 -100
  273. package/src/pages/PlanningPage.tsx +17 -14
  274. package/src/pages/RaiseDetail.tsx +5 -0
  275. package/src/pages/RoleDetail.tsx +8 -0
  276. package/src/pages/SearchPage.tsx +2 -1
  277. package/src/pages/account/PreferencesPage.tsx +2 -37
  278. package/src/pages/admin/DataPage.tsx +63 -21
  279. package/src/pages/admin/FieldsPage.tsx +1 -1
  280. package/src/pages/admin/ModulesPage.tsx +1 -0
  281. package/src/pages/admin/WorkspaceSettingsPage.tsx +10 -33
  282. package/src/pages/auth/AuthLayout.tsx +8 -4
  283. package/src/pages/auth/RegionSwitcher.tsx +56 -0
  284. package/src/pages/auth/SignInPage.tsx +10 -6
  285. package/src/pages/auth/SignUpPage.tsx +25 -0
  286. package/src/pages/auth/regionHref.ts +10 -0
  287. package/src/pages/dashboard/DashboardPage.tsx +16 -48
  288. package/src/pages/dashboard/attention.ts +24 -78
  289. package/src/pages/dashboard/sections.tsx +8 -2
  290. package/src/pages/forms/FormActions.tsx +14 -8
  291. package/src/pages/onboarding/HandbookStepPage.tsx +21 -24
  292. package/src/pages/onboarding/InvitesStepPage.tsx +27 -19
  293. package/src/pages/onboarding/ModulesStepPage.tsx +145 -0
  294. package/src/pages/onboarding/OnboardingNav.tsx +97 -0
  295. package/src/pages/onboarding/OrganisationStepPage.tsx +130 -0
  296. package/src/pages/onboarding/WorkspaceStepPage.tsx +53 -38
  297. package/src/pages/onboarding/handbookTemplates.ts +77 -0
  298. package/src/pages/onboarding/onboardingModules.ts +202 -0
  299. package/src/pages/onboarding/onboardingRerun.ts +41 -0
  300. package/src/styles.css +30 -0
@@ -3,16 +3,16 @@ import type {
3
3
  DashboardPlanItem,
4
4
  DashboardStaleContact,
5
5
  DashboardTouchpoint,
6
- PipelineKind,
6
+ DashboardUpcomingEvent,
7
7
  RecordTargetType,
8
8
  } from '@kelpie/schemas'
9
9
 
10
10
  import type { ChipTone } from '../../components/Chip.tsx'
11
- import { formatDay } from '../../lib/dates.ts'
11
+ import { formatDate, formatDay } from '../../lib/dates.ts'
12
12
 
13
13
  /**
14
- * Reading the workspace snapshot as a page: one attention list, and the brief
15
- * above it.
14
+ * Reading the workspace snapshot as a page: one attention list from the
15
+ * signals the service already ranked.
16
16
  *
17
17
  * Pure functions over the decoded response, so the page renders and these are
18
18
  * tested separately. Nothing here recomputes a signal: the service already
@@ -30,6 +30,8 @@ const ROUTES: Readonly<Record<RecordTargetType, string | undefined>> = {
30
30
  raise: '/fundraising',
31
31
  enquiry: '/enquiries',
32
32
  candidate: undefined,
33
+ event: '/events',
34
+ attendance: undefined,
33
35
  }
34
36
 
35
37
  const TARGET_TYPE_LABELS: Readonly<Record<RecordTargetType, string>> = {
@@ -41,6 +43,8 @@ const TARGET_TYPE_LABELS: Readonly<Record<RecordTargetType, string>> = {
41
43
  raise: 'Fundraising',
42
44
  enquiry: 'Enquiry',
43
45
  candidate: 'Candidate',
46
+ event: 'Event',
47
+ attendance: 'Attendance',
44
48
  }
45
49
 
46
50
  export function targetTypeLabel(targetType: RecordTargetType): string {
@@ -115,8 +119,22 @@ function staleContactRow(contact: DashboardStaleContact): AttentionRow {
115
119
  }
116
120
  }
117
121
 
122
+ function upcomingEventRow(event: DashboardUpcomingEvent, timezone: string): AttentionRow {
123
+ const where = event.location.length > 0 ? event.location : event.format
124
+
125
+ return {
126
+ id: `event-${event.id}`,
127
+ label: 'Upcoming event',
128
+ tone: 'accent',
129
+ meta: formatDate(event.startsAt, timezone),
130
+ title: event.name,
131
+ detail: `${String(event.attendeeCount)} registered · ${where}`,
132
+ href: `/events/${event.id}`,
133
+ }
134
+ }
135
+
118
136
  /**
119
- * The signals behind the brief, most urgent first.
137
+ * The attention list, most urgent first.
120
138
  *
121
139
  * The groups are concatenated rather than sorted: each arrives from the API in
122
140
  * its own order — plan items soonest first, contacts coldest first — and a
@@ -131,79 +149,7 @@ export function attentionRows(dashboard: Dashboard): readonly AttentionRow[] {
131
149
  ...touchpoints.filter((touchpoint) => touchpoint.overdue).map(touchpointRow),
132
150
  ...dashboard.dueSoonPlanItems.items.map((item) => planItemRow(item, false)),
133
151
  ...touchpoints.filter((touchpoint) => !touchpoint.overdue).map(touchpointRow),
152
+ ...dashboard.upcomingEvents.items.map((item) => upcomingEventRow(item, dashboard.timezone)),
134
153
  ...dashboard.staleContacts.items.map(staleContactRow),
135
154
  ]
136
155
  }
137
-
138
- function plural(count: number, singular: string, pluralForm = `${singular}s`): string {
139
- return `${String(count)} ${count === 1 ? singular : pluralForm}`
140
- }
141
-
142
- /**
143
- * How a count of open records reads mid-sentence.
144
- *
145
- * Not `PIPELINE_KIND_LABELS`: that one is nav wording, where a Raise is filed
146
- * under "Fundraising", and "3 fundraisings" is not a sentence. Written out
147
- * rather than suffixed, because "opportunitys" is not one either.
148
- */
149
- const OPEN_PIPELINE_NOUNS: Readonly<
150
- Record<PipelineKind, { readonly one: string; readonly many: string }>
151
- > = {
152
- enquiry: { one: 'enquiry', many: 'enquiries' },
153
- deal: { one: 'deal', many: 'deals' },
154
- opportunity: { one: 'opportunity', many: 'opportunities' },
155
- raise: { one: 'raise', many: 'raises' },
156
- partnership: { one: 'partnership', many: 'partnerships' },
157
- }
158
-
159
- /**
160
- * The brief, built from the totals rather than from the rows.
161
- *
162
- * A list is capped by the request's limit and a total is not, so counting the
163
- * rows on screen would understate every workspace with more than a page of them.
164
- */
165
- export function briefLines(dashboard: Dashboard): readonly string[] {
166
- const lines: string[] = []
167
-
168
- if (dashboard.overduePlanItems.total > 0) {
169
- lines.push(
170
- `${plural(dashboard.overduePlanItems.total, 'plan item')} overdue — triage Planning first.`,
171
- )
172
- }
173
-
174
- if (dashboard.dueSoonPlanItems.total > 0) {
175
- lines.push(
176
- `${plural(dashboard.dueSoonPlanItems.total, 'plan item')} due in the next ${plural(dashboard.upcomingDays, 'day')}.`,
177
- )
178
- }
179
-
180
- if (dashboard.partnershipTouchpoints.total > 0) {
181
- lines.push(
182
- `${plural(dashboard.partnershipTouchpoints.total, 'partnership touchpoint')} at hand.`,
183
- )
184
- }
185
-
186
- if (dashboard.staleContacts.total > 0) {
187
- lines.push(
188
- `${plural(dashboard.staleContacts.total, 'contact')} past the ${String(dashboard.staleContactDays)}-day touch threshold.`,
189
- )
190
- }
191
-
192
- const open = dashboard.pipelines.filter((pipeline) => pipeline.open > 0)
193
-
194
- if (open.length > 0) {
195
- lines.push(
196
- `Open: ${open
197
- .map((pipeline) => {
198
- const noun = OPEN_PIPELINE_NOUNS[pipeline.kind]
199
-
200
- return plural(pipeline.open, noun.one, noun.many)
201
- })
202
- .join(', ')}.`,
203
- )
204
- }
205
-
206
- return lines.length === 0
207
- ? ['Nothing urgent. Pipeline and relationships look quiet today.']
208
- : lines
209
- }
@@ -9,6 +9,7 @@ import type { ReactNode } from 'react'
9
9
  import { Link } from 'react-router'
10
10
 
11
11
  import { Chip } from '../../components/Chip.tsx'
12
+ import { MarkdownView } from '../../components/MarkdownView.tsx'
12
13
  import { SectionHeader } from '../../components/SectionHeader.tsx'
13
14
  import { formatDate, formatRelativeTime } from '../../lib/dates.ts'
14
15
  import { targetHref, targetTypeLabel } from './attention.ts'
@@ -72,7 +73,10 @@ function EmptyOr({
72
73
  export function AttentionList({ rows }: { readonly rows: readonly AttentionRow[] }): React.JSX.Element {
73
74
  return (
74
75
  <section>
75
- <SectionHeader title="Needs attention" description="The signals behind the brief." />
76
+ <SectionHeader
77
+ title="Needs attention"
78
+ description="Overdue Plans, partnership touchpoints, and contacts going cold."
79
+ />
76
80
  <EmptyOr isEmpty={rows.length === 0} empty="Nothing urgent right now.">
77
81
  <ul className="divide-y divide-border border-y border-border">
78
82
  {rows.map((row) => (
@@ -201,7 +205,9 @@ export function NotesList({
201
205
  {notes.map((note) => (
202
206
  <li key={note.id} className="py-3">
203
207
  <div className="flex items-start justify-between gap-2">
204
- <p className="line-clamp-3 text-[13px] leading-relaxed text-ink">{note.body}</p>
208
+ <div className="line-clamp-3 min-w-0 flex-1">
209
+ <MarkdownView source={note.body} />
210
+ </div>
205
211
  {note.pinned && (
206
212
  <span className="shrink-0 text-[10px] font-medium tracking-wide text-accent uppercase">
207
213
  Pinned
@@ -1,9 +1,10 @@
1
- import { PIPELINE_KINDS } from '@kelpie/schemas'
2
- import type { Form, FormAttachTarget, PipelineKind } from '@kelpie/schemas'
1
+ import { FORM_ATTACH_TARGET_TYPES, PLAN_ITEM_TARGET_TYPE_LABELS } from '@kelpie/schemas'
2
+ import type { Form, FormAttachTarget, FormAttachTargetType, PipelineKind } from '@kelpie/schemas'
3
3
  import { useMemo, useState } from 'react'
4
4
 
5
5
  import { useDeals } from '../../api/resources/deals.ts'
6
6
  import { useEnquiries } from '../../api/resources/enquiries.ts'
7
+ import { useEvents } from '../../api/resources/events.ts'
7
8
  import { useUpdateForm } from '../../api/resources/forms.ts'
8
9
  import { useLists } from '../../api/resources/lists.ts'
9
10
  import { useMembers } from '../../api/resources/members.ts'
@@ -400,12 +401,13 @@ interface AttachRecord {
400
401
  readonly name: string
401
402
  }
402
403
 
403
- function useAttachOptions(kind: PipelineKind): readonly AttachRecord[] {
404
+ function useAttachOptions(kind: FormAttachTargetType): readonly AttachRecord[] {
404
405
  const deals = useDeals({}, { enabled: kind === 'deal' })
405
406
  const opportunities = useOpportunities({}, { enabled: kind === 'opportunity' })
406
407
  const raises = useRaises({}, { enabled: kind === 'raise' })
407
408
  const partnerships = usePartnerships({}, { enabled: kind === 'partnership' })
408
409
  const enquiries = useEnquiries({}, { enabled: kind === 'enquiry' })
410
+ const events = useEvents({}, { enabled: kind === 'event' })
409
411
 
410
412
  switch (kind) {
411
413
  case 'deal':
@@ -421,6 +423,8 @@ function useAttachOptions(kind: PipelineKind): readonly AttachRecord[] {
421
423
  }))
422
424
  case 'enquiry':
423
425
  return enquiries.records.map((enquiry) => ({ id: enquiry.id, name: enquiry.name }))
426
+ case 'event':
427
+ return events.records.map((record) => ({ id: record.id, name: record.name }))
424
428
  }
425
429
  }
426
430
 
@@ -431,7 +435,7 @@ function AttachTargetsBlock({
431
435
  readonly form: Form
432
436
  readonly onChange: (next: readonly FormAttachTarget[]) => void
433
437
  }): React.JSX.Element {
434
- const [kind, setKind] = useState<PipelineKind>('deal')
438
+ const [kind, setKind] = useState<FormAttachTargetType>('deal')
435
439
  const [pick, setPick] = useState('')
436
440
  const options = useAttachOptions(kind)
437
441
  const chosen = form.attachTargets
@@ -459,7 +463,9 @@ function AttachTargetsBlock({
459
463
  <div className="space-y-2 rounded-md border border-border p-4">
460
464
  <div className="text-[13px] font-medium text-ink">Attach the submitter to a record</div>
461
465
  <p className="text-[11px] text-ink-faint">
462
- Every submitter is linked into these records through <code>person_links</code>.
466
+ Pipeline records receive a <code>person_links</code> row. An Event
467
+ registers Attendance instead: the submitter is marked registered, and a
468
+ second submit of the same person is a no-op.
463
469
  </p>
464
470
  <ul className="divide-y divide-border">
465
471
  {chosen.length === 0 && (
@@ -496,13 +502,13 @@ function AttachTargetsBlock({
496
502
  className={inputClass}
497
503
  value={kind}
498
504
  onChange={(event) => {
499
- setKind(event.target.value as PipelineKind)
505
+ setKind(event.target.value as FormAttachTargetType)
500
506
  setPick('')
501
507
  }}
502
508
  >
503
- {PIPELINE_KINDS.map((option) => (
509
+ {FORM_ATTACH_TARGET_TYPES.map((option) => (
504
510
  <option key={option} value={option}>
505
- {option}
511
+ {PLAN_ITEM_TARGET_TYPE_LABELS[option]}
506
512
  </option>
507
513
  ))}
508
514
  </select>
@@ -1,45 +1,47 @@
1
- import { useNavigate } from 'react-router'
1
+ import { Navigate, useNavigate, useSearchParams } from 'react-router'
2
2
 
3
3
  import { useHandbookPages } from '../../api/resources/handbookPages.ts'
4
4
  import { ErrorPanel, LoadingPanel } from '../../components/QueryState.tsx'
5
- import { PrimaryButton } from '../auth/AuthForm.tsx'
6
5
  import { AuthLayout } from '../auth/AuthLayout.tsx'
6
+ import { OnboardingNav } from './OnboardingNav.tsx'
7
+ import { isOnboardingRerun } from './onboardingRerun.ts'
7
8
 
8
9
  /**
9
- * Onboarding step 3: the starter handbook.
10
+ * Onboarding step 5: review the starter handbook.
10
11
  *
11
- * The mockup's button created these pages. Against the real API they already
12
- * exist: `POST /v1/workspaces` seeded them in the same transaction that created
13
- * the workspace, two steps ago. So this reads them back and shows what is there.
12
+ * Pages were seeded on step 2 when the reader picked an organisation type.
13
+ * This step reads them back and shows what agents will learn from.
14
14
  *
15
- * That is the deviation, and it is the honest one. A button labelled "create"
16
- * that creates nothing teaches a reader something false about the product, and
17
- * the alternative — deleting the step — would drop the one screen that tells a
18
- * new workspace its handbook is what agents read.
15
+ * A rerun skips this step: the pages are already in place.
19
16
  */
20
17
  export function HandbookStepPage(): React.JSX.Element {
21
18
  const navigate = useNavigate()
19
+ const [searchParams] = useSearchParams()
22
20
  const { records: pages, isLoading, error } = useHandbookPages()
23
21
 
22
+ if (isOnboardingRerun(searchParams)) {
23
+ return <Navigate to="/dashboard" replace />
24
+ }
25
+
24
26
  function finish(): void {
25
- // The wizard ends on the dashboard, per `onboarding.md`. It is empty for a
26
- // brand new workspace, which is the point: it says what there is to fill in.
27
27
  navigate('/dashboard', { replace: true })
28
28
  }
29
29
 
30
+ const sortedPages = [...pages].sort((left, right) => left.sortOrder - right.sortOrder)
31
+
30
32
  return (
31
33
  <AuthLayout
32
- step={3}
34
+ step={5}
33
35
  title="Your starter handbook"
34
- description="These markdown pages are ready. Agents read them to learn what your company is."
36
+ description="These markdown pages were chosen for your organisation type. Agents read them to learn what your company is."
35
37
  >
36
38
  <div className="mt-5 space-y-4">
37
39
  {error !== null && <ErrorPanel error={error} />}
38
40
  {error === null && isLoading && <LoadingPanel label="Loading your handbook…" />}
39
41
 
40
- {error === null && !isLoading && pages.length > 0 && (
42
+ {error === null && !isLoading && sortedPages.length > 0 && (
41
43
  <ul className="max-h-56 overflow-y-auto rounded-md border border-border bg-surface">
42
- {pages.map((page) => (
44
+ {sortedPages.map((page) => (
43
45
  <li
44
46
  key={page.id}
45
47
  className="border-b border-border px-3 py-2 text-[13px] text-ink last:border-0"
@@ -50,25 +52,20 @@ export function HandbookStepPage(): React.JSX.Element {
50
52
  </ul>
51
53
  )}
52
54
 
53
- {/* An empty box under a heading that says the pages are ready would
54
- assert one thing and show another. The handbook is seeded with the
55
- workspace, so nothing here is the seeding having not happened. */}
56
- {error === null && !isLoading && pages.length === 0 && (
55
+ {error === null && !isLoading && sortedPages.length === 0 && (
57
56
  <p className="rounded-md border border-border bg-surface px-3 py-2 text-[13px] text-ink-muted">
58
57
  This workspace has no handbook pages. That is not what a new workspace should look
59
58
  like — the Handbook tab is where to add them.
60
59
  </p>
61
60
  )}
62
61
 
63
- {pages.length > 0 && (
62
+ {sortedPages.length > 0 && (
64
63
  <p className="text-[12px] text-ink-muted">
65
64
  Every page starts as a stub. Write them from the Handbook tab whenever you are ready.
66
65
  </p>
67
66
  )}
68
67
 
69
- {/* Not a submit: the work behind this step already happened, so the
70
- button only moves the reader on. */}
71
- <PrimaryButton label="Go to Kelpie" onClick={finish} />
68
+ <OnboardingNav step={5} nextLabel="Go to Kelpie" nextType="button" onNext={finish} />
72
69
  </div>
73
70
  </AuthLayout>
74
71
  )
@@ -2,14 +2,16 @@ import { INVITABLE_ROLES } from '@kelpie/schemas'
2
2
  import type { InvitableRole } from '@kelpie/schemas'
3
3
  import { useState } from 'react'
4
4
  import type { FormEvent } from 'react'
5
- import { useNavigate } from 'react-router'
5
+ import { useNavigate, useSearchParams } from 'react-router'
6
6
 
7
7
  import { useSendInvite } from '../../api/resources/invites.ts'
8
- import { AUTH_INPUT_CLASS, SecondaryButton, SubmitButton } from '../auth/AuthForm.tsx'
8
+ import { AUTH_INPUT_CLASS, SecondaryButton } from '../auth/AuthForm.tsx'
9
9
  import { AuthLayout } from '../auth/AuthLayout.tsx'
10
+ import { OnboardingNav } from './OnboardingNav.tsx'
11
+ import { isOnboardingRerun } from './onboardingRerun.ts'
10
12
 
11
13
  /**
12
- * Onboarding step 2: invitations, against `POST /v1/workspaces/:id/invites`.
14
+ * Onboarding step 4: invitations, against `POST /v1/workspaces/:id/invites`.
13
15
  *
14
16
  * One request per address, and they can fail one at a time — an address already
15
17
  * invited answers `409` while its neighbours succeed. The mockup could ignore
@@ -38,9 +40,16 @@ function isOutstanding(row: InviteRow): boolean {
38
40
 
39
41
  export function InvitesStepPage(): React.JSX.Element {
40
42
  const navigate = useNavigate()
43
+ const [searchParams] = useSearchParams()
44
+ const rerun = isOnboardingRerun(searchParams)
41
45
  const sendInvite = useSendInvite()
42
46
  const [rows, setRows] = useState<readonly InviteRow[]>([emptyRow()])
43
47
 
48
+ /** First run reviews the handbook next; a rerun leaves those pages alone. */
49
+ function afterInvites(): void {
50
+ navigate(rerun ? '/dashboard' : '/onboarding/handbook', { replace: true })
51
+ }
52
+
44
53
  function changeRow(index: number, changes: Partial<InviteRow>): void {
45
54
  setRows((previous) =>
46
55
  previous.map((row, at) => (at === index ? { ...row, ...changes } : row)),
@@ -80,7 +89,7 @@ export function InvitesStepPage(): React.JSX.Element {
80
89
  sendOutstanding()
81
90
  .then((allSent) => {
82
91
  if (allSent) {
83
- navigate('/onboarding/handbook', { replace: true })
92
+ afterInvites()
84
93
  }
85
94
  })
86
95
  .catch(() => undefined)
@@ -90,7 +99,7 @@ export function InvitesStepPage(): React.JSX.Element {
90
99
 
91
100
  return (
92
101
  <AuthLayout
93
- step={2}
102
+ step={4}
94
103
  title="Invite teammates"
95
104
  description="Optional. You can always invite people later from Admin → Team."
96
105
  >
@@ -117,20 +126,19 @@ export function InvitesStepPage(): React.JSX.Element {
117
126
  Add another
118
127
  </button>
119
128
 
120
- <div className="flex flex-col gap-2 pt-1">
121
- <SubmitButton
122
- label={outstanding === 0 ? 'Continue' : 'Send invitations'}
123
- pendingLabel="Sending…"
124
- isPending={sendInvite.isPending}
125
- />
126
- <SecondaryButton
127
- label="Skip for now"
128
- disabled={sendInvite.isPending}
129
- onClick={() => {
130
- navigate('/onboarding/handbook', { replace: true })
131
- }}
132
- />
133
- </div>
129
+ <OnboardingNav
130
+ step={4}
131
+ nextLabel={outstanding === 0 ? 'Next' : 'Send invitations'}
132
+ nextPendingLabel="Sending…"
133
+ isPending={sendInvite.isPending}
134
+ extra={
135
+ <SecondaryButton
136
+ label="Skip for now"
137
+ disabled={sendInvite.isPending}
138
+ onClick={afterInvites}
139
+ />
140
+ }
141
+ />
134
142
  </form>
135
143
  </AuthLayout>
136
144
  )
@@ -0,0 +1,145 @@
1
+ import { useState } from 'react'
2
+ import type { FormEvent } from 'react'
3
+ import { useNavigate, useSearchParams } from 'react-router'
4
+
5
+ import { useSetModuleEnabled } from '../../api/resources/moduleSettings.ts'
6
+ import { useInstallSampleData } from '../../api/resources/sampleData.ts'
7
+ import { useSession } from '../../api/resources/session.ts'
8
+ import { ErrorPanel, LoadingPanel } from '../../components/QueryState.tsx'
9
+ import { AuthLayout } from '../auth/AuthLayout.tsx'
10
+ import { organisationChoiceFromParam, organisationChoiceLabel } from './handbookTemplates.ts'
11
+ import type { OrganisationChoiceId } from './handbookTemplates.ts'
12
+ import { OnboardingNav } from './OnboardingNav.tsx'
13
+ import { defaultOnboardingModuleState, onboardingModulesForChoice } from './onboardingModules.ts'
14
+ import { isOnboardingRerun, ONBOARDING_ORG_PARAM, onboardingPath } from './onboardingRerun.ts'
15
+
16
+ function modulesStepDescription(org: OrganisationChoiceId): string {
17
+ if (org === 'startup' || org === 'later') {
18
+ return 'Pick the parts of Kelpie your team needs today. Turn the rest on whenever you like — nothing here is permanent.'
19
+ }
20
+
21
+ return `Suggested for ${organisationChoiceLabel(org)}. Turn the rest on whenever you like — nothing here is permanent.`
22
+ }
23
+
24
+ /**
25
+ * Onboarding step 3: which optional modules this workspace runs.
26
+ *
27
+ * The workspace already exists. This step writes each of the six choices
28
+ * through `PATCH /v1/workspaces/:id/modules/:moduleId`, then optionally
29
+ * installs sample data, then moves on to invites. Defaults and explanations
30
+ * follow the organisation type on `?org=`. Other toggleable modules stay
31
+ * on; Admin → Modules is where they change later.
32
+ */
33
+
34
+ export function ModulesStepPage(): React.JSX.Element {
35
+ const navigate = useNavigate()
36
+ const [searchParams] = useSearchParams()
37
+ const rerun = isOnboardingRerun(searchParams)
38
+ const org = organisationChoiceFromParam(searchParams.get(ONBOARDING_ORG_PARAM))
39
+ const modules = onboardingModulesForChoice(org)
40
+ const { session, isLoading } = useSession()
41
+ const setEnabled = useSetModuleEnabled()
42
+ const installSampleData = useInstallSampleData()
43
+ const [enabled, setEnabledState] = useState(() => defaultOnboardingModuleState(org))
44
+ const [seedSample, setSeedSample] = useState(false)
45
+ const [saving, setSaving] = useState(false)
46
+
47
+ function toggle(moduleId: (typeof modules)[number]['id'], next: boolean): void {
48
+ setEnabledState((previous) => ({ ...previous, [moduleId]: next }))
49
+ }
50
+
51
+ function submit(event: FormEvent): void {
52
+ event.preventDefault()
53
+
54
+ const workspaceId = session?.workspaceId
55
+
56
+ if (workspaceId === null || workspaceId === undefined) {
57
+ return
58
+ }
59
+
60
+ setSaving(true)
61
+
62
+ persistChoices()
63
+ .then(async () => {
64
+ if (seedSample) {
65
+ await installSampleData.runAsync({ workspaceId })
66
+ }
67
+
68
+ navigate(onboardingPath('/onboarding/invites', rerun, org), { replace: true })
69
+ })
70
+ .catch(() => undefined)
71
+ .finally(() => {
72
+ setSaving(false)
73
+ })
74
+ }
75
+
76
+ async function persistChoices(): Promise<void> {
77
+ for (const choice of modules) {
78
+ await setEnabled.runAsync({
79
+ moduleId: choice.id,
80
+ enabled: enabled[choice.id] === true,
81
+ })
82
+ }
83
+ }
84
+
85
+ const isPending = saving || setEnabled.isPending || installSampleData.isPending
86
+ const workspaceId = session?.workspaceId
87
+ const description = modulesStepDescription(org)
88
+
89
+ if (isLoading || workspaceId === null || workspaceId === undefined) {
90
+ return (
91
+ <AuthLayout step={3} title="What do you want to use?" description={description}>
92
+ <div className="mt-5">
93
+ <LoadingPanel label="Loading…" />
94
+ </div>
95
+ </AuthLayout>
96
+ )
97
+ }
98
+
99
+ return (
100
+ <AuthLayout step={3} title="What do you want to use?" description={description}>
101
+ <form onSubmit={submit} className="mt-5 space-y-3">
102
+ <ul className="space-y-3">
103
+ {modules.map((choice) => (
104
+ <li key={choice.id}>
105
+ <label className="flex items-start gap-2 text-[12px] text-ink">
106
+ <input
107
+ type="checkbox"
108
+ checked={enabled[choice.id] === true}
109
+ onChange={(event) => {
110
+ toggle(choice.id, event.target.checked)
111
+ }}
112
+ className="mt-0.5 h-4 w-4 rounded border-border text-accent focus:ring-accent"
113
+ />
114
+ <span>
115
+ <span className="font-medium">{choice.label}</span>
116
+ <span className="mt-0.5 block text-[11px] text-ink-muted">{choice.description}</span>
117
+ </span>
118
+ </label>
119
+ </li>
120
+ ))}
121
+ </ul>
122
+ <label className="flex items-start gap-2 text-[12px] text-ink">
123
+ <input
124
+ type="checkbox"
125
+ checked={seedSample}
126
+ onChange={(event) => setSeedSample(event.target.checked)}
127
+ className="mt-0.5 h-4 w-4 rounded border-border text-accent focus:ring-accent"
128
+ />
129
+ <span>
130
+ Fill it with sample data — a few companies, people, and records for the parts you
131
+ turned on, so you are not staring at an empty screen. Clear it out any time.
132
+ </span>
133
+ </label>
134
+ {setEnabled.error !== null && <ErrorPanel error={setEnabled.error} />}
135
+ {installSampleData.error !== null && <ErrorPanel error={installSampleData.error} />}
136
+ <OnboardingNav
137
+ step={3}
138
+ nextLabel="Next"
139
+ nextPendingLabel={installSampleData.isPending ? 'Installing sample data…' : 'Saving…'}
140
+ isPending={isPending}
141
+ />
142
+ </form>
143
+ </AuthLayout>
144
+ )
145
+ }
@@ -0,0 +1,97 @@
1
+ import type { ReactNode } from 'react'
2
+ import { useNavigate, useSearchParams } from 'react-router'
3
+
4
+ import type { OnboardingStep } from '../auth/AuthLayout.tsx'
5
+ import { isOnboardingRerun, ONBOARDING_ORG_PARAM, onboardingPath } from './onboardingRerun.ts'
6
+
7
+ /**
8
+ * The Previous / Next row every onboarding step shares.
9
+ *
10
+ * Forward still commits on the step that owns the form. This only moves
11
+ * between the five routes. Step 1 has no previous onboarding page, so
12
+ * Previous stays off there. The `rerun` and `org` queries are kept on
13
+ * Previous so a finished workspace that walks the wizard again still skips
14
+ * handbook seeding, and the modules step still sees the organisation type.
15
+ */
16
+
17
+ const ONBOARDING_PATHS = {
18
+ 1: '/onboarding/workspace',
19
+ 2: '/onboarding/organisation',
20
+ 3: '/onboarding/modules',
21
+ 4: '/onboarding/invites',
22
+ 5: '/onboarding/handbook',
23
+ } as const
24
+
25
+ const PRIMARY_CLASS =
26
+ 'flex-1 rounded-md bg-accent px-3.5 py-2 text-[12px] font-semibold text-accent-fg transition hover:bg-accent-hover disabled:opacity-50'
27
+
28
+ const SECONDARY_CLASS =
29
+ 'flex-1 rounded-md border border-border bg-surface px-3.5 py-2 text-[12px] font-medium text-ink-muted transition hover:bg-surface-raised disabled:opacity-50'
30
+
31
+ export interface OnboardingNavProps {
32
+ readonly step: OnboardingStep
33
+ readonly nextLabel: string
34
+ /** Shown while the step's request is in flight. */
35
+ readonly nextPendingLabel?: string
36
+ readonly isPending?: boolean
37
+ /** `submit` inside a form; `button` when the step only navigates. */
38
+ readonly nextType?: 'submit' | 'button'
39
+ readonly onNext?: () => void
40
+ /** A third action under the row, such as Skip on invites. */
41
+ readonly extra?: ReactNode
42
+ }
43
+
44
+ export function previousOnboardingPath(
45
+ step: OnboardingStep,
46
+ rerun = false,
47
+ org?: string | null,
48
+ ): string | undefined {
49
+ if (step === 1) {
50
+ return undefined
51
+ }
52
+
53
+ return onboardingPath(ONBOARDING_PATHS[(step - 1) as OnboardingStep], rerun, org)
54
+ }
55
+
56
+ export function OnboardingNav({
57
+ step,
58
+ nextLabel,
59
+ nextPendingLabel,
60
+ isPending = false,
61
+ nextType = 'submit',
62
+ onNext,
63
+ extra,
64
+ }: OnboardingNavProps): React.JSX.Element {
65
+ const navigate = useNavigate()
66
+ const [searchParams] = useSearchParams()
67
+ const rerun = isOnboardingRerun(searchParams)
68
+ const previousTo = previousOnboardingPath(step, rerun, searchParams.get(ONBOARDING_ORG_PARAM))
69
+
70
+ return (
71
+ <div className="flex flex-col gap-2 pt-1">
72
+ <div className="flex gap-2">
73
+ {previousTo !== undefined && (
74
+ <button
75
+ type="button"
76
+ disabled={isPending}
77
+ onClick={() => {
78
+ navigate(previousTo, { replace: true })
79
+ }}
80
+ className={SECONDARY_CLASS}
81
+ >
82
+ Previous
83
+ </button>
84
+ )}
85
+ <button
86
+ type={nextType}
87
+ disabled={isPending}
88
+ onClick={onNext}
89
+ className={PRIMARY_CLASS}
90
+ >
91
+ {isPending && nextPendingLabel !== undefined ? nextPendingLabel : nextLabel}
92
+ </button>
93
+ </div>
94
+ {extra}
95
+ </div>
96
+ )
97
+ }