@kelpie/ui 0.11.0 → 0.13.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 (333) 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 +25 -6
  95. package/dist/index.d.ts.map +1 -1
  96. package/dist/index.js +17 -3
  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/account/SecurityPage.js +2 -2
  156. package/dist/pages/account/SecurityPage.js.map +1 -1
  157. package/dist/pages/admin/DataPage.js +10 -9
  158. package/dist/pages/admin/DataPage.js.map +1 -1
  159. package/dist/pages/admin/FieldsPage.js +1 -1
  160. package/dist/pages/admin/FieldsPage.js.map +1 -1
  161. package/dist/pages/admin/ModulesPage.d.ts.map +1 -1
  162. package/dist/pages/admin/ModulesPage.js +1 -0
  163. package/dist/pages/admin/ModulesPage.js.map +1 -1
  164. package/dist/pages/admin/WorkspaceSettingsPage.d.ts +12 -0
  165. package/dist/pages/admin/WorkspaceSettingsPage.d.ts.map +1 -1
  166. package/dist/pages/admin/WorkspaceSettingsPage.js +2 -21
  167. package/dist/pages/admin/WorkspaceSettingsPage.js.map +1 -1
  168. package/dist/pages/auth/AuthForm.d.ts +18 -0
  169. package/dist/pages/auth/AuthForm.d.ts.map +1 -1
  170. package/dist/pages/auth/AuthForm.js +17 -0
  171. package/dist/pages/auth/AuthForm.js.map +1 -1
  172. package/dist/pages/auth/AuthLayout.d.ts +5 -4
  173. package/dist/pages/auth/AuthLayout.d.ts.map +1 -1
  174. package/dist/pages/auth/AuthLayout.js +7 -5
  175. package/dist/pages/auth/AuthLayout.js.map +1 -1
  176. package/dist/pages/auth/AuthMethods.d.ts +12 -0
  177. package/dist/pages/auth/AuthMethods.d.ts.map +1 -0
  178. package/dist/pages/auth/AuthMethods.js +20 -0
  179. package/dist/pages/auth/AuthMethods.js.map +1 -0
  180. package/dist/pages/auth/RegionSwitcher.d.ts +10 -0
  181. package/dist/pages/auth/RegionSwitcher.d.ts.map +1 -0
  182. package/dist/pages/auth/RegionSwitcher.js +28 -0
  183. package/dist/pages/auth/RegionSwitcher.js.map +1 -0
  184. package/dist/pages/auth/SignInPage.d.ts +7 -0
  185. package/dist/pages/auth/SignInPage.d.ts.map +1 -1
  186. package/dist/pages/auth/SignInPage.js +5 -8
  187. package/dist/pages/auth/SignInPage.js.map +1 -1
  188. package/dist/pages/auth/SignUpPage.d.ts.map +1 -1
  189. package/dist/pages/auth/SignUpPage.js +9 -1
  190. package/dist/pages/auth/SignUpPage.js.map +1 -1
  191. package/dist/pages/auth/nextPath.d.ts +15 -0
  192. package/dist/pages/auth/nextPath.d.ts.map +1 -0
  193. package/dist/pages/auth/nextPath.js +17 -0
  194. package/dist/pages/auth/nextPath.js.map +1 -0
  195. package/dist/pages/auth/regionHref.d.ts +6 -0
  196. package/dist/pages/auth/regionHref.d.ts.map +1 -0
  197. package/dist/pages/auth/regionHref.js +8 -0
  198. package/dist/pages/auth/regionHref.js.map +1 -0
  199. package/dist/pages/dashboard/DashboardPage.d.ts +8 -0
  200. package/dist/pages/dashboard/DashboardPage.d.ts.map +1 -1
  201. package/dist/pages/dashboard/DashboardPage.js +5 -13
  202. package/dist/pages/dashboard/DashboardPage.js.map +1 -1
  203. package/dist/pages/dashboard/attention.d.ts +1 -8
  204. package/dist/pages/dashboard/attention.d.ts.map +1 -1
  205. package/dist/pages/dashboard/attention.js +21 -54
  206. package/dist/pages/dashboard/attention.js.map +1 -1
  207. package/dist/pages/dashboard/sections.d.ts.map +1 -1
  208. package/dist/pages/dashboard/sections.js +3 -2
  209. package/dist/pages/dashboard/sections.js.map +1 -1
  210. package/dist/pages/forms/FormActions.d.ts.map +1 -1
  211. package/dist/pages/forms/FormActions.js +7 -3
  212. package/dist/pages/forms/FormActions.js.map +1 -1
  213. package/dist/pages/onboarding/HandbookStepPage.d.ts +4 -8
  214. package/dist/pages/onboarding/HandbookStepPage.d.ts.map +1 -1
  215. package/dist/pages/onboarding/HandbookStepPage.js +13 -13
  216. package/dist/pages/onboarding/HandbookStepPage.js.map +1 -1
  217. package/dist/pages/onboarding/InvitesStepPage.d.ts.map +1 -1
  218. package/dist/pages/onboarding/InvitesStepPage.js +13 -7
  219. package/dist/pages/onboarding/InvitesStepPage.js.map +1 -1
  220. package/dist/pages/onboarding/ModulesStepPage.d.ts +11 -0
  221. package/dist/pages/onboarding/ModulesStepPage.d.ts.map +1 -0
  222. package/dist/pages/onboarding/ModulesStepPage.js +80 -0
  223. package/dist/pages/onboarding/ModulesStepPage.js.map +1 -0
  224. package/dist/pages/onboarding/OnboardingNav.d.ts +17 -0
  225. package/dist/pages/onboarding/OnboardingNav.d.ts.map +1 -0
  226. package/dist/pages/onboarding/OnboardingNav.js +37 -0
  227. package/dist/pages/onboarding/OnboardingNav.js.map +1 -0
  228. package/dist/pages/onboarding/OrganisationStepPage.d.ts +13 -0
  229. package/dist/pages/onboarding/OrganisationStepPage.d.ts.map +1 -0
  230. package/dist/pages/onboarding/OrganisationStepPage.js +59 -0
  231. package/dist/pages/onboarding/OrganisationStepPage.js.map +1 -0
  232. package/dist/pages/onboarding/WorkspaceStepPage.d.ts.map +1 -1
  233. package/dist/pages/onboarding/WorkspaceStepPage.js +25 -20
  234. package/dist/pages/onboarding/WorkspaceStepPage.js.map +1 -1
  235. package/dist/pages/onboarding/handbookTemplates.d.ts +19 -0
  236. package/dist/pages/onboarding/handbookTemplates.d.ts.map +1 -0
  237. package/dist/pages/onboarding/handbookTemplates.js +57 -0
  238. package/dist/pages/onboarding/handbookTemplates.js.map +1 -0
  239. package/dist/pages/onboarding/onboardingModules.d.ts +23 -0
  240. package/dist/pages/onboarding/onboardingModules.d.ts.map +1 -0
  241. package/dist/pages/onboarding/onboardingModules.js +153 -0
  242. package/dist/pages/onboarding/onboardingModules.js.map +1 -0
  243. package/dist/pages/onboarding/onboardingRerun.d.ts +21 -0
  244. package/dist/pages/onboarding/onboardingRerun.d.ts.map +1 -0
  245. package/dist/pages/onboarding/onboardingRerun.js +33 -0
  246. package/dist/pages/onboarding/onboardingRerun.js.map +1 -0
  247. package/dist/registry/context.d.ts +3 -1
  248. package/dist/registry/context.d.ts.map +1 -1
  249. package/dist/registry/context.js +4 -0
  250. package/dist/registry/context.js.map +1 -1
  251. package/dist/registry/contributions.d.ts +24 -0
  252. package/dist/registry/contributions.d.ts.map +1 -1
  253. package/dist/registry/contributions.js.map +1 -1
  254. package/dist/registry/registry.d.ts +4 -1
  255. package/dist/registry/registry.d.ts.map +1 -1
  256. package/dist/registry/registry.js +6 -0
  257. package/dist/registry/registry.js.map +1 -1
  258. package/package.json +4 -3
  259. package/src/api/resources/attendances.ts +54 -0
  260. package/src/api/resources/eventAssociations.ts +43 -0
  261. package/src/api/resources/events.ts +70 -0
  262. package/src/api/resources/handbookSeed.ts +63 -0
  263. package/src/api/resources/moduleSettings.ts +25 -0
  264. package/src/api/resources/planItems.ts +5 -5
  265. package/src/api/resources/publicConfig.ts +3 -3
  266. package/src/api/resources/sampleData.ts +2 -0
  267. package/src/api/resources/session.ts +59 -0
  268. package/src/api/resources/workspace.ts +2 -3
  269. package/src/app/KelpieApp.tsx +11 -3
  270. package/src/components/ActivitiesPanel.tsx +2 -0
  271. package/src/components/AddressesField.tsx +502 -0
  272. package/src/components/CountrySearch.tsx +71 -0
  273. package/src/components/EnvironmentBanner.tsx +23 -6
  274. package/src/components/InlineEdit.tsx +35 -1
  275. package/src/components/LinkedEventsPanel.tsx +50 -0
  276. package/src/components/NotesPanel.tsx +132 -15
  277. package/src/components/PlanAttention.tsx +5 -4
  278. package/src/components/PlanPanel.tsx +3 -3
  279. package/src/components/Shell.tsx +16 -1
  280. package/src/components/SocialNetworkIcon.tsx +88 -0
  281. package/src/components/SocialProfilesField.tsx +57 -10
  282. package/src/components/TimezoneSearch.tsx +67 -0
  283. package/src/components/WorkspaceSwitcher.tsx +136 -0
  284. package/src/components/environmentTone.ts +40 -0
  285. package/src/index.ts +49 -3
  286. package/src/lib/timezones.ts +43 -0
  287. package/src/pages/CompaniesPage.tsx +14 -13
  288. package/src/pages/CompanyDetail.tsx +59 -27
  289. package/src/pages/DealDetail.tsx +5 -0
  290. package/src/pages/DecisionsPage.tsx +7 -0
  291. package/src/pages/EnquiryDetail.tsx +7 -0
  292. package/src/pages/EventDetail.tsx +794 -0
  293. package/src/pages/EventsPage.tsx +382 -0
  294. package/src/pages/ListDetail.tsx +24 -0
  295. package/src/pages/OpportunityDetail.tsx +7 -0
  296. package/src/pages/PartnershipDetail.tsx +7 -0
  297. package/src/pages/PeoplePage.tsx +18 -5
  298. package/src/pages/PersonDetail.tsx +300 -100
  299. package/src/pages/PlanningPage.tsx +17 -14
  300. package/src/pages/RaiseDetail.tsx +5 -0
  301. package/src/pages/RoleDetail.tsx +8 -0
  302. package/src/pages/SearchPage.tsx +2 -1
  303. package/src/pages/account/PreferencesPage.tsx +2 -37
  304. package/src/pages/account/SecurityPage.tsx +6 -1
  305. package/src/pages/admin/DataPage.tsx +63 -21
  306. package/src/pages/admin/FieldsPage.tsx +1 -1
  307. package/src/pages/admin/ModulesPage.tsx +1 -0
  308. package/src/pages/admin/WorkspaceSettingsPage.tsx +10 -33
  309. package/src/pages/auth/AuthForm.tsx +38 -0
  310. package/src/pages/auth/AuthLayout.tsx +8 -4
  311. package/src/pages/auth/AuthMethods.tsx +29 -0
  312. package/src/pages/auth/RegionSwitcher.tsx +56 -0
  313. package/src/pages/auth/SignInPage.tsx +13 -14
  314. package/src/pages/auth/SignUpPage.tsx +32 -0
  315. package/src/pages/auth/nextPath.ts +17 -0
  316. package/src/pages/auth/regionHref.ts +10 -0
  317. package/src/pages/dashboard/DashboardPage.tsx +16 -48
  318. package/src/pages/dashboard/attention.ts +24 -78
  319. package/src/pages/dashboard/sections.tsx +8 -2
  320. package/src/pages/forms/FormActions.tsx +14 -8
  321. package/src/pages/onboarding/HandbookStepPage.tsx +21 -24
  322. package/src/pages/onboarding/InvitesStepPage.tsx +27 -19
  323. package/src/pages/onboarding/ModulesStepPage.tsx +145 -0
  324. package/src/pages/onboarding/OnboardingNav.tsx +97 -0
  325. package/src/pages/onboarding/OrganisationStepPage.tsx +130 -0
  326. package/src/pages/onboarding/WorkspaceStepPage.tsx +53 -38
  327. package/src/pages/onboarding/handbookTemplates.ts +77 -0
  328. package/src/pages/onboarding/onboardingModules.ts +202 -0
  329. package/src/pages/onboarding/onboardingRerun.ts +41 -0
  330. package/src/registry/context.ts +6 -0
  331. package/src/registry/contributions.ts +26 -0
  332. package/src/registry/registry.ts +12 -0
  333. package/src/styles.css +30 -0
@@ -11,6 +11,7 @@ import {
11
11
  useConvertPipelineRecord,
12
12
  } from '../api/resources/conversions.ts'
13
13
  import { useMembers } from '../api/resources/members.ts'
14
+ import { useModuleEnabled } from '../api/resources/moduleSettings.ts'
14
15
  import { usePeople } from '../api/resources/people.ts'
15
16
  import { usePipelineStages } from '../api/resources/pipelineStages.ts'
16
17
  import { useRecordPlanItems } from '../api/resources/planItems.ts'
@@ -27,6 +28,7 @@ import { CustomFieldsPanel } from '../components/CustomFieldsPanel.tsx'
27
28
  import { useHasCustomFields } from '../components/useHasCustomFields.ts'
28
29
  import { InlineEdit } from '../components/InlineEdit.tsx'
29
30
  import { ListsPanel } from '../components/ListsPanel.tsx'
31
+ import { LinkedEventsPanel } from '../components/LinkedEventsPanel.tsx'
30
32
  import { NotesPanel } from '../components/NotesPanel.tsx'
31
33
  import { PlanAttention } from '../components/PlanAttention.tsx'
32
34
  import { PlanPanel } from '../components/PlanPanel.tsx'
@@ -69,6 +71,7 @@ export function RaiseDetail(): React.JSX.Element {
69
71
  const deleteRaise = useDeleteRaise()
70
72
  const moduleTabs = inSlotOrder(useRecordTabs('raise'))
71
73
  const hasCustomFields = useHasCustomFields('raise')
74
+ const eventsEnabled = useModuleEnabled('events')
72
75
  const [activeTab, setActiveTab] = useState('overview')
73
76
  const [showConvert, setShowConvert] = useState(false)
74
77
 
@@ -92,6 +95,7 @@ export function RaiseDetail(): React.JSX.Element {
92
95
  { id: 'notes', label: 'Notes' },
93
96
  { id: 'decisions', label: 'Decisions' },
94
97
  { id: 'lists', label: 'Lists' },
98
+ ...(eventsEnabled ? [{ id: 'events', label: 'Events' }] : []),
95
99
  ...moduleTabs.map((tab) => ({ id: tab.id, label: tab.label })),
96
100
  ]
97
101
  const active = tabs.some((tab) => tab.id === activeTab) ? activeTab : 'overview'
@@ -162,6 +166,7 @@ export function RaiseDetail(): React.JSX.Element {
162
166
  {active === 'notes' && <NotesPanel targetType="raise" targetId={record.id} />}
163
167
  {active === 'decisions' && <DecisionsPanel targetType="raise" targetId={record.id} />}
164
168
  {active === 'lists' && <ListsPanel targetType="raise" targetId={record.id} />}
169
+ {active === 'events' && <LinkedEventsPanel targetType="raise" targetId={record.id} />}
165
170
  {moduleTab?.render({ objectType: 'raise', recordId: record.id })}
166
171
  </RecordTabs>
167
172
  </div>
@@ -7,6 +7,7 @@ import { Link, useNavigate, useParams } from 'react-router'
7
7
  import { usePatch } from '../api/resource.ts'
8
8
  import type { PatchResult } from '../api/resource.ts'
9
9
  import { useCandidates, useCreateCandidate, useDeleteCandidate } from '../api/resources/candidates.ts'
10
+ import { useModuleEnabled } from '../api/resources/moduleSettings.ts'
10
11
  import { useCreateNote, useDeleteNote, useUpdateNote } from '../api/resources/notes.ts'
11
12
  import { useCreatePerson, usePeople } from '../api/resources/people.ts'
12
13
  import { useDeleteRole, useRole, useUpdateRole } from '../api/resources/roles.ts'
@@ -15,6 +16,7 @@ import { Chip } from '../components/Chip.tsx'
15
16
  import { DeleteRecord } from '../components/DeleteRecord.tsx'
16
17
  import { EntitySearch } from '../components/EntitySearch.tsx'
17
18
  import { InlineEdit } from '../components/InlineEdit.tsx'
19
+ import { LinkedEventsPanel } from '../components/LinkedEventsPanel.tsx'
18
20
  import { Paginator } from '../components/Paginator.tsx'
19
21
  import { ErrorPanel, LoadingPanel, NotFoundPanel } from '../components/QueryState.tsx'
20
22
  import { SectionHeader } from '../components/SectionHeader.tsx'
@@ -46,6 +48,7 @@ export function RoleDetail(): React.JSX.Element {
46
48
  const navigate = useNavigate()
47
49
  const { record, isLoading, isNotFound, error } = useRole(id)
48
50
  const deleteRole = useDeleteRole()
51
+ const eventsEnabled = useModuleEnabled('events')
49
52
 
50
53
  if (isNotFound) {
51
54
  return <NotFoundPanel label="Role" backTo="/hiring" />
@@ -89,6 +92,11 @@ export function RoleDetail(): React.JSX.Element {
89
92
  </div>
90
93
 
91
94
  <RoleCandidates role={record} />
95
+ {eventsEnabled && (
96
+ <div className="mt-8">
97
+ <LinkedEventsPanel targetType="role" targetId={record.id} />
98
+ </div>
99
+ )}
92
100
  </div>
93
101
  )
94
102
  }
@@ -24,6 +24,7 @@ const COLLECTIONS: Readonly<Record<SearchCollection, { label: string; path: (id:
24
24
  opportunity: { label: 'Opportunities', path: (id) => `/opportunities/${id}` },
25
25
  raise: { label: 'Fundraising', path: (id) => `/fundraising/${id}` },
26
26
  partnership: { label: 'Partnerships', path: (id) => `/partnerships/${id}` },
27
+ event: { label: 'Events', path: (id) => `/events/${id}` },
27
28
  // Decisions have no detail page of their own, in the mockup or here. The global
28
29
  // list is where one is read.
29
30
  decision: { label: 'Decisions', path: () => '/decisions' },
@@ -39,7 +40,7 @@ export function SearchPage(): React.JSX.Element {
39
40
  <div className="animate-fade-in py-16 text-center">
40
41
  <p className="text-[15px] font-medium text-ink">Search Kelpie</p>
41
42
  <p className="mt-1 text-[13px] text-ink-muted">
42
- Find people, companies, deals, raises, decisions, and handbook pages.
43
+ Find people, companies, deals, events, raises, decisions, and handbook pages.
43
44
  </p>
44
45
  </div>
45
46
  )
@@ -8,9 +8,9 @@ import {
8
8
  useTheme,
9
9
  useUpdateAccountPreferences,
10
10
  } from '../../api/resources/account.ts'
11
- import { EntitySearch } from '../../components/EntitySearch.tsx'
12
11
  import { PageHeader } from '../../components/PageHeader.tsx'
13
12
  import { ErrorPanel, LoadingPanel } from '../../components/QueryState.tsx'
13
+ import { TimezoneSearch } from '../../components/TimezoneSearch.tsx'
14
14
  import { Field } from './Field.tsx'
15
15
 
16
16
  /**
@@ -27,31 +27,6 @@ const THEME_LABELS: Readonly<Record<ThemePreference, string>> = {
27
27
  dark: 'Dark',
28
28
  }
29
29
 
30
- /**
31
- * Every zone this runtime's ICU knows, matching what the server's
32
- * `timezoneSchema` will accept. Falls back to the zones the mockup offered if
33
- * `Intl.supportedValuesOf` is missing, for a browser old enough to lack it.
34
- *
35
- * `UTC` is prepended because `supportedValuesOf` omits it in some engines even
36
- * though ECMA-402 guarantees `Intl.DateTimeFormat` accepts it as a zone name.
37
- * Without this, a very common choice would be searchable only by accident of
38
- * already being the stored value.
39
- */
40
- const ALL_TIMEZONES: readonly string[] = (() => {
41
- try {
42
- return ['UTC', ...Intl.supportedValuesOf('timeZone')]
43
- } catch {
44
- return [
45
- 'UTC',
46
- 'Australia/Sydney',
47
- 'Australia/Melbourne',
48
- 'America/Los_Angeles',
49
- 'America/New_York',
50
- 'Europe/London',
51
- ]
52
- }
53
- })()
54
-
55
30
  export function PreferencesPage(): React.JSX.Element {
56
31
  const { preferences, isLoading, error } = useAccountPreferences()
57
32
 
@@ -79,8 +54,6 @@ function PreferencesForm({
79
54
  const [productUpdates, setProductUpdates] = useState(preferences.productUpdates)
80
55
  const [saved, setSaved] = useState(false)
81
56
 
82
- const zones = ALL_TIMEZONES.includes(timezone) ? ALL_TIMEZONES : [timezone, ...ALL_TIMEZONES]
83
-
84
57
  function save(event: FormEvent): void {
85
58
  event.preventDefault()
86
59
  setSaved(false)
@@ -135,15 +108,7 @@ function PreferencesForm({
135
108
  label="Timezone"
136
109
  hint="Stored on your account. Dates across Kelpie use this zone."
137
110
  >
138
- <EntitySearch
139
- options={zones.map((zone) => ({ id: zone, label: zone }))}
140
- value={timezone}
141
- onChange={setTimezone}
142
- placeholder="Search time zones…"
143
- limit={20}
144
- required
145
- size="md"
146
- />
111
+ <TimezoneSearch value={timezone} onChange={setTimezone} required size="md" />
147
112
  </Field>
148
113
  </div>
149
114
 
@@ -88,7 +88,8 @@ function ChangePasswordForm(): React.JSX.Element {
88
88
  <form onSubmit={submit} className="space-y-3">
89
89
  <h2 className="text-[14px] font-semibold text-ink">Change password</h2>
90
90
  <p className="max-w-md text-[12px] text-ink-muted">
91
- Changing your password signs you out everywhere except this browser.
91
+ Changing your password signs you out everywhere except this browser. If you have never set
92
+ one, use <span className="font-medium text-ink">Forgot password</span> on the sign-in page.
92
93
  </p>
93
94
 
94
95
  <label className="block max-w-md">
@@ -216,6 +217,10 @@ function SessionRow({
216
217
  <td className="px-4 py-3">
217
218
  <div className="font-medium break-all text-ink">{session.device ?? 'Unknown device'}</div>
218
219
  {session.current && <span className="text-[11px] font-medium text-success">This device</span>}
220
+ {/* Null for a password sign-in, which is the ordinary case and needs no label. */}
221
+ {session.signedInVia !== null && (
222
+ <div className="text-[11px] text-ink-faint">via {session.signedInVia}</div>
223
+ )}
219
224
  </td>
220
225
  {/* Always unknown today: the service records the User-Agent and nothing
221
226
  derives a location from the request. The column stays because the field
@@ -1014,17 +1014,17 @@ function messageOf(error: Error | null | undefined): string | null {
1014
1014
  /**
1015
1015
  * One button that fills the workspace with a small demo fixture.
1016
1016
  *
1017
- * The endpoint refuses on a workspace that already has data, so the button
1018
- * never doubles the seed and the "already has data" answer surfaces here as
1019
- * a plain error line.
1017
+ * The first click opens a warning. Confirm is what POSTs. Existing records
1018
+ * stay; a sample email or domain that is already present fails the install.
1020
1019
  */
1021
1020
  function SampleDataSection(): ReactNode {
1022
1021
  const { session } = useSession()
1023
1022
  const workspaceId = session?.workspaceId ?? null
1024
1023
  const install = useInstallSampleData()
1025
1024
  const [counts, setCounts] = useState<SampleDataCounts | null>(null)
1025
+ const [confirming, setConfirming] = useState(false)
1026
1026
 
1027
- function onInstall(): void {
1027
+ function onConfirm(): void {
1028
1028
  if (workspaceId === null) {
1029
1029
  return
1030
1030
  }
@@ -1033,6 +1033,7 @@ function SampleDataSection(): ReactNode {
1033
1033
  .runAsync({ workspaceId })
1034
1034
  .then((result) => {
1035
1035
  setCounts(result)
1036
+ setConfirming(false)
1036
1037
  })
1037
1038
  .catch(() => undefined)
1038
1039
  }
@@ -1044,32 +1045,73 @@ function SampleDataSection(): ReactNode {
1044
1045
  <div>
1045
1046
  <h2 className="text-[15px] font-semibold text-ink">Install sample data</h2>
1046
1047
  <p className="mt-1 text-[13px] text-ink-muted">
1047
- A small set of companies, people, and deals so a fresh workspace has something
1048
- to look at. Refuses if this workspace already has CRM data.
1048
+ A small set of companies, people, deals, and events so a workspace has something to look
1049
+ at. Existing records stay. This page asks before it writes.
1049
1050
  </p>
1050
1051
  </div>
1051
- <button
1052
- type="button"
1053
- onClick={onInstall}
1054
- disabled={install.isPending || workspaceId === null || counts !== null}
1055
- className="rounded-md bg-accent px-3.5 py-2 text-[12px] font-semibold text-accent-fg transition hover:bg-accent-hover disabled:opacity-50"
1056
- >
1057
- {counts !== null
1058
- ? 'Installed'
1059
- : install.isPending
1060
- ? 'Installing…'
1061
- : 'Install sample data'}
1062
- </button>
1052
+ {counts !== null ? (
1053
+ <button
1054
+ type="button"
1055
+ disabled
1056
+ className="rounded-md bg-accent px-3.5 py-2 text-[12px] font-semibold text-accent-fg opacity-50"
1057
+ >
1058
+ Installed
1059
+ </button>
1060
+ ) : confirming ? (
1061
+ <div className="rounded-md border border-warning bg-warning-soft p-3 text-[13px] text-ink">
1062
+ <p>
1063
+ This adds sample companies, people, and other CRM records. Existing records are not
1064
+ removed. If sample emails or domains are already present, the install fails.
1065
+ </p>
1066
+ {failure === null ? null : (
1067
+ <p role="alert" className="mt-2 text-[12px] font-medium text-danger">
1068
+ {failure}
1069
+ </p>
1070
+ )}
1071
+ <div className="mt-2 flex justify-end gap-2">
1072
+ <button
1073
+ type="button"
1074
+ onClick={() => {
1075
+ setConfirming(false)
1076
+ }}
1077
+ disabled={install.isPending}
1078
+ className="rounded-md border border-border px-3 py-1 text-[12px] hover:bg-surface"
1079
+ >
1080
+ Cancel
1081
+ </button>
1082
+ <button
1083
+ type="button"
1084
+ onClick={onConfirm}
1085
+ disabled={install.isPending || workspaceId === null}
1086
+ className="rounded-md bg-accent px-3 py-1 text-[12px] font-semibold text-accent-fg hover:bg-accent-hover disabled:opacity-50"
1087
+ >
1088
+ {install.isPending ? 'Installing…' : 'Install sample data'}
1089
+ </button>
1090
+ </div>
1091
+ </div>
1092
+ ) : (
1093
+ <button
1094
+ type="button"
1095
+ onClick={() => {
1096
+ setConfirming(true)
1097
+ }}
1098
+ disabled={workspaceId === null}
1099
+ className="rounded-md bg-accent px-3.5 py-2 text-[12px] font-semibold text-accent-fg transition hover:bg-accent-hover disabled:opacity-50"
1100
+ >
1101
+ Install sample data
1102
+ </button>
1103
+ )}
1063
1104
  {counts !== null ? (
1064
1105
  <p className="text-[12px] text-ink-muted">
1065
1106
  Added {counts.companies} companies, {counts.people} people, {counts.positions}{' '}
1066
1107
  positions, {counts.deals} deals, {counts.opportunities} opportunities,{' '}
1067
1108
  {counts.raises} raises, {counts.partnerships} partnerships, {counts.enquiries}{' '}
1068
- enquiries, {counts.roles} roles, {counts.candidates} candidates,{' '}
1069
- {counts.planItems} plan items, and {counts.notes} notes.
1109
+ enquiries, {counts.roles} roles, {counts.candidates} candidates, {counts.events}{' '}
1110
+ events, {counts.attendances} attendances, {counts.planItems} plan items, and{' '}
1111
+ {counts.notes} notes.
1070
1112
  </p>
1071
1113
  ) : null}
1072
- {failure === null ? null : (
1114
+ {confirming || failure === null ? null : (
1073
1115
  <p role="alert" className="text-[12px] font-medium text-danger">
1074
1116
  {failure}
1075
1117
  </p>
@@ -52,7 +52,7 @@ export function FieldsPage(): React.JSX.Element {
52
52
  <div className="animate-slide-in mx-auto max-w-4xl space-y-6">
53
53
  <PageHeader
54
54
  title="Custom fields"
55
- description="Add workspace-defined fields to the six main record types. Values on records go through the same API the UI uses."
55
+ description="Add workspace-defined fields to People, Companies, pipeline records, and Events. Values on records go through the same API the UI uses."
56
56
  />
57
57
  {isAdmin ? <FieldsAdmin /> : <MemberNotice />}
58
58
  </div>
@@ -23,6 +23,7 @@ const MODULE_LABELS: Readonly<Record<string, string>> = {
23
23
  opportunities: 'Opportunities',
24
24
  partnerships: 'Partnerships',
25
25
  raises: 'Fundraising',
26
+ events: 'Events',
26
27
  hiring: 'Hiring',
27
28
  handbook: 'Handbook',
28
29
  forms: 'Forms',
@@ -18,6 +18,7 @@ import { EntitySearch } from '../../components/EntitySearch.tsx'
18
18
  import type { SearchOption } from '../../components/EntitySearch.tsx'
19
19
  import { PageHeader } from '../../components/PageHeader.tsx'
20
20
  import { ErrorPanel, LoadingPanel } from '../../components/QueryState.tsx'
21
+ import { TimezoneSearch } from '../../components/TimezoneSearch.tsx'
21
22
 
22
23
  /**
23
24
  * Workspace settings, and the one screen that can end a workspace.
@@ -32,22 +33,6 @@ import { ErrorPanel, LoadingPanel } from '../../components/QueryState.tsx'
32
33
  * would be a stream of half-typed addresses.
33
34
  */
34
35
 
35
- /**
36
- * The timezones the mockup offered, plus whatever this workspace already holds.
37
- *
38
- * A full IANA list belongs in a combo box, which is its own piece of work. An
39
- * unlisted value must still survive a save, so it joins the list rather than
40
- * being silently rewritten to the first option.
41
- */
42
- const COMMON_TIMEZONES = [
43
- 'Australia/Sydney',
44
- 'Australia/Melbourne',
45
- 'America/Los_Angeles',
46
- 'America/New_York',
47
- 'Europe/London',
48
- 'UTC',
49
- ] as const
50
-
51
36
  export function WorkspaceSettingsPage(): React.JSX.Element {
52
37
  const { workspace, isLoading, error } = useWorkspace()
53
38
 
@@ -77,9 +62,6 @@ function WorkspaceSettingsForm({
77
62
  const [saved, setSaved] = useState(false)
78
63
 
79
64
  const canEdit = session?.role === 'owner' || session?.role === 'admin'
80
- const timezones = COMMON_TIMEZONES.includes(timezone as (typeof COMMON_TIMEZONES)[number])
81
- ? [...COMMON_TIMEZONES]
82
- : [timezone, ...COMMON_TIMEZONES]
83
65
 
84
66
  function save(event: FormEvent): void {
85
67
  event.preventDefault()
@@ -130,20 +112,15 @@ function WorkspaceSettingsForm({
130
112
  </Field>
131
113
 
132
114
  <Field label="Timezone">
133
- <select
134
- value={timezone}
135
- onChange={(event) => {
136
- setTimezone(event.target.value)
137
- }}
138
- disabled={!canEdit}
139
- className="w-full max-w-md rounded-md border border-border bg-surface-raised px-3 py-2 text-[13px] outline-none focus:border-accent disabled:opacity-60"
140
- >
141
- {timezones.map((zone) => (
142
- <option key={zone} value={zone}>
143
- {zone}
144
- </option>
145
- ))}
146
- </select>
115
+ <div className="max-w-md">
116
+ <TimezoneSearch
117
+ value={timezone}
118
+ onChange={setTimezone}
119
+ required
120
+ disabled={!canEdit}
121
+ size="md"
122
+ />
123
+ </div>
147
124
  </Field>
148
125
 
149
126
  {update.error !== null && (
@@ -116,3 +116,41 @@ export function SecondaryButton({
116
116
  </button>
117
117
  )
118
118
  }
119
+
120
+ /**
121
+ * A labelled rule, for a module separating its own buttons from the password
122
+ * form above them. Core never renders one itself; see `AuthMethods`.
123
+ */
124
+ export function AuthDivider({ label = 'or' }: { readonly label?: string }): React.JSX.Element {
125
+ return (
126
+ <div className="flex items-center gap-3 py-1">
127
+ <span className="h-px flex-1 bg-border" />
128
+ <span className="text-[11px] text-ink-faint">{label}</span>
129
+ <span className="h-px flex-1 bg-border" />
130
+ </div>
131
+ )
132
+ }
133
+
134
+ /**
135
+ * The secondary button as a link.
136
+ *
137
+ * A plain anchor, not a router link: a sign-in method leaves the app for its
138
+ * provider, and the destination is a server route the SPA router knows nothing
139
+ * about.
140
+ */
141
+ export function AuthLinkButton({
142
+ label,
143
+ href,
144
+ }: {
145
+ readonly label: string
146
+ readonly href: string
147
+ }): React.JSX.Element {
148
+ return (
149
+ <a
150
+ href={href}
151
+ className="block w-full rounded-md border border-border bg-surface px-3.5 py-2 text-center text-[12px] font-medium text-ink-muted transition hover:bg-surface-raised"
152
+ >
153
+ {label}
154
+ </a>
155
+ )
156
+ }
@@ -1,17 +1,20 @@
1
1
  import type { ReactNode } from 'react'
2
2
  import { Link } from 'react-router'
3
3
 
4
+ import { RegionSwitcher } from './RegionSwitcher.tsx'
5
+
4
6
  /**
5
7
  * The centred card every signed-out page sits in: sign in, signup, the two
6
- * password-reset pages, and the three onboarding steps.
8
+ * password-reset pages, and the five onboarding steps.
7
9
  *
8
10
  * `step` is what makes it an onboarding page. Onboarding is a sequence somebody
9
11
  * is part-way through, and a card that does not say where they are in it turns
10
- * three screens into three unrelated forms.
12
+ * five screens into five unrelated forms. The region switcher stays off
13
+ * those steps: a session already exists on this origin.
11
14
  */
12
15
 
13
- /** Onboarding, per `onboarding.md`: workspace, invites, handbook. */
14
- const ONBOARDING_STEPS = [1, 2, 3] as const
16
+ /** Onboarding, per `onboarding.md`: workspace, organisation, modules, invites, handbook. */
17
+ const ONBOARDING_STEPS = [1, 2, 3, 4, 5] as const
15
18
 
16
19
  export type OnboardingStep = (typeof ONBOARDING_STEPS)[number]
17
20
 
@@ -50,6 +53,7 @@ export function AuthLayout({
50
53
  {children}
51
54
  {footer !== undefined && <div className="mt-4 text-center text-[12px]">{footer}</div>}
52
55
  </div>
56
+ {step === undefined && <RegionSwitcher />}
53
57
  </div>
54
58
  )
55
59
  }
@@ -0,0 +1,29 @@
1
+ import { Fragment } from 'react'
2
+
3
+ import { useAuthMethods } from '../../registry/context.ts'
4
+ import type { AuthMethodContext } from '../../registry/contributions.ts'
5
+
6
+ /**
7
+ * The `auth.methods` slot, rendered by the signed-out pages beside their form.
8
+ *
9
+ * Nothing is drawn around the contributions: no divider, no "or" rule. Whether
10
+ * a module has anything to show is decided at run time (a sign-in module with
11
+ * no provider configured renders nothing), so core framing would leave a
12
+ * heading over an empty space. A module that draws buttons draws its own
13
+ * divider with `AuthDivider`.
14
+ */
15
+ export function AuthMethods({ intent, next }: AuthMethodContext): React.JSX.Element | null {
16
+ const methods = useAuthMethods()
17
+
18
+ if (methods.length === 0) {
19
+ return null
20
+ }
21
+
22
+ return (
23
+ <div className="mt-4 space-y-2">
24
+ {methods.map((method) => (
25
+ <Fragment key={method.id}>{method.render({ intent, next })}</Fragment>
26
+ ))}
27
+ </div>
28
+ )
29
+ }
@@ -0,0 +1,56 @@
1
+ import type { ChangeEvent } from 'react'
2
+
3
+ import { usePublicConfig } from '../../api/resources/publicConfig.ts'
4
+ import { regionHref } from './regionHref.ts'
5
+
6
+ /**
7
+ * A region picker for signed-out pages.
8
+ *
9
+ * Hidden unless public config lists two or more origins. Selecting another
10
+ * region leaves this origin: the session cookie is host-scoped, so the
11
+ * browser has to load the other host's login page. Path, query, and hash
12
+ * stay, so `/login?next=` and invite tokens survive the hop.
13
+ */
14
+
15
+ export function RegionSwitcher(): React.JSX.Element | null {
16
+ const { config } = usePublicConfig()
17
+ const regions = config?.regions ?? []
18
+
19
+ if (regions.length < 2) {
20
+ return null
21
+ }
22
+
23
+ const current = regions.find((region) => region.origin === window.location.origin)
24
+
25
+ function change(event: ChangeEvent<HTMLSelectElement>): void {
26
+ const selected = regions.find((region) => region.id === event.target.value)
27
+
28
+ if (selected === undefined || selected.origin === window.location.origin) {
29
+ return
30
+ }
31
+
32
+ window.location.assign(regionHref(selected.origin, window.location))
33
+ }
34
+
35
+ return (
36
+ <label className="mt-6 block w-full max-w-sm">
37
+ <span className="mb-1.5 block text-[12px] font-medium text-ink">Region</span>
38
+ <select
39
+ className="w-full rounded-md border border-border bg-transparent px-2 py-1 text-[13px] outline-none focus:border-accent"
40
+ value={current?.id ?? ''}
41
+ onChange={change}
42
+ >
43
+ {current === undefined && (
44
+ <option value="" disabled>
45
+ Select a region
46
+ </option>
47
+ )}
48
+ {regions.map((region) => (
49
+ <option key={region.id} value={region.id}>
50
+ {region.label}
51
+ </option>
52
+ ))}
53
+ </select>
54
+ </label>
55
+ )
56
+ }
@@ -2,10 +2,13 @@ import { useState } from 'react'
2
2
  import type { FormEvent } from 'react'
3
3
  import { Link, useNavigate, useSearchParams } from 'react-router'
4
4
 
5
+ import { usePublicConfig } from '../../api/resources/publicConfig.ts'
5
6
  import { useLogIn } from '../../api/resources/session.ts'
6
7
  import { ErrorPanel } from '../../components/QueryState.tsx'
7
8
  import { SubmitButton, TextField } from './AuthForm.tsx'
8
9
  import { AuthLayout } from './AuthLayout.tsx'
10
+ import { AuthMethods } from './AuthMethods.tsx'
11
+ import { safeNext } from './nextPath.ts'
9
12
 
10
13
  /**
11
14
  * Sign in, against `POST /v1/auth/login`.
@@ -15,18 +18,11 @@ import { AuthLayout } from './AuthLayout.tsx'
15
18
  * People with the invitation still unaccepted.
16
19
  */
17
20
 
18
- /**
19
- * Only a path within this app. An absolute URL in `?next=` would turn the
20
- * sign-in form into an open redirect.
21
- */
22
- function safeNext(value: string | null): string {
23
- return value !== null && value.startsWith('/') && !value.startsWith('//') ? value : '/dashboard'
24
- }
25
-
26
21
  export function SignInPage(): React.JSX.Element {
27
22
  const navigate = useNavigate()
28
23
  const [searchParameters] = useSearchParams()
29
24
  const logIn = useLogIn()
25
+ const { config } = usePublicConfig()
30
26
  const [email, setEmail] = useState('')
31
27
  const [password, setPassword] = useState('')
32
28
 
@@ -49,12 +45,14 @@ export function SignInPage(): React.JSX.Element {
49
45
  <Link to="/forgot-password" className="font-medium text-accent hover:underline">
50
46
  Forgot password?
51
47
  </Link>
52
- <span className="text-ink-muted">
53
- No account?{' '}
54
- <Link to="/signup" className="font-medium text-accent hover:underline">
55
- Sign up
56
- </Link>
57
- </span>
48
+ {config?.signupsEnabled !== false && (
49
+ <span className="text-ink-muted">
50
+ No account?{' '}
51
+ <Link to="/signup" className="font-medium text-accent hover:underline">
52
+ Sign up
53
+ </Link>
54
+ </span>
55
+ )}
58
56
  </div>
59
57
  }
60
58
  >
@@ -78,6 +76,7 @@ export function SignInPage(): React.JSX.Element {
78
76
  {logIn.error !== null && <ErrorPanel error={logIn.error} />}
79
77
  <SubmitButton label="Sign in" pendingLabel="Signing in…" isPending={logIn.isPending} />
80
78
  </form>
79
+ <AuthMethods intent="login" next={next} />
81
80
  </AuthLayout>
82
81
  )
83
82
  }