@tribe-nest/forge 3.59.0 → 3.61.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 (266) hide show
  1. package/package.json +2 -1
  2. package/src/_tests/testCommunity.tsx +28 -0
  3. package/src/contexts/CartContext.tsx +34 -1
  4. package/src/contexts/CommunityContext.tsx +192 -0
  5. package/src/contexts/PublicAuthContext.tsx +15 -3
  6. package/src/data/queries/_tests/metaSignals.spec.tsx +130 -0
  7. package/src/data/queries/useAccount.ts +33 -2
  8. package/src/data/queries/useAnalytics.ts +6 -6
  9. package/src/data/queries/useCallChat.ts +2 -2
  10. package/src/data/queries/useChat.ts +52 -40
  11. package/src/data/queries/useCheckouts.ts +2 -1
  12. package/src/data/queries/useCoachingAvailability.ts +10 -1
  13. package/src/data/queries/useCommunities.ts +345 -0
  14. package/src/data/queries/useCommunity.ts +450 -102
  15. package/src/data/queries/useCommunityAgent.ts +247 -0
  16. package/src/data/queries/useCommunityCalendar.ts +150 -0
  17. package/src/data/queries/useCommunityClassroom.ts +75 -0
  18. package/src/data/queries/useCommunityMembers.ts +108 -0
  19. package/src/data/queries/useCommunityModeration.ts +327 -0
  20. package/src/data/queries/useCommunityWiki.ts +137 -0
  21. package/src/data/queries/useCourses.ts +2 -0
  22. package/src/data/queries/useEmailSignIn.ts +79 -0
  23. package/src/data/queries/useEvents.ts +6 -1
  24. package/src/data/queries/useForms.ts +5 -1
  25. package/src/data/queries/useLeadMagnet.ts +6 -2
  26. package/src/data/queries/useMembership.ts +32 -7
  27. package/src/data/queries/useMembershipCalls.ts +116 -7
  28. package/src/data/queries/useMyBookings.ts +24 -1
  29. package/src/data/queries/useMyTickets.ts +24 -1
  30. package/src/data/queries/useNotifications.ts +6 -2
  31. package/src/data/queries/useOrders.ts +2 -1
  32. package/src/data/queries/usePWA.ts +111 -35
  33. package/src/data/queries/usePostCollections.ts +28 -16
  34. package/src/data/queries/usePosts.ts +16 -14
  35. package/src/data/queries/useSiteApp.ts +130 -0
  36. package/src/data/queries/useSubscriptions.ts +28 -9
  37. package/src/i18n/de.json +891 -2
  38. package/src/i18n/en.json +891 -2
  39. package/src/index.ts +36 -7
  40. package/src/runtime/pages.spec.ts +36 -0
  41. package/src/runtime/pages.ts +52 -27
  42. package/src/server/_tests/buildWebManifest.spec.ts +1 -1
  43. package/src/server/_tests/forgeServiceWorkerScript.spec.ts +4 -4
  44. package/src/types/models.ts +11 -0
  45. package/src/ui/analytics/ForgeAnalytics.tsx +64 -34
  46. package/src/ui/analytics/PageMetaPixel.tsx +23 -8
  47. package/src/ui/analytics/_tests/ForgeAnalytics.meta.spec.tsx +129 -0
  48. package/src/ui/analytics/_tests/metaCommerceEvents.spec.tsx +159 -0
  49. package/src/ui/analytics/useMetaEvents.ts +78 -0
  50. package/src/ui/format/_tests/membershipGate.spec.ts +6 -6
  51. package/src/ui/format/membershipGate.ts +19 -15
  52. package/src/ui/headless/_tests/communityAgent.spec.tsx +519 -0
  53. package/src/ui/headless/_tests/communityCalendar.spec.tsx +195 -0
  54. package/src/ui/headless/_tests/communityClassroom.spec.tsx +120 -0
  55. package/src/ui/headless/_tests/communityGate.spec.tsx +80 -0
  56. package/src/ui/headless/_tests/communityHarness.tsx +212 -0
  57. package/src/ui/headless/_tests/communityMembers.spec.tsx +316 -0
  58. package/src/ui/headless/_tests/communityPoll.spec.tsx +389 -0
  59. package/src/ui/headless/_tests/communityProvider.spec.tsx +151 -0
  60. package/src/ui/headless/_tests/memberHomeBar.spec.ts +84 -0
  61. package/src/ui/headless/_tests/siteAppBar.spec.ts +83 -0
  62. package/src/ui/headless/_tests/siteAppHarness.ts +19 -0
  63. package/src/ui/headless/_tests/siteAppHome.spec.ts +226 -0
  64. package/src/ui/headless/_tests/siteAppPrompts.spec.ts +185 -0
  65. package/src/ui/headless/_tests/useCommunityInvite.spec.tsx +104 -0
  66. package/src/ui/headless/_tests/useCommunityJoin.spec.tsx +596 -0
  67. package/src/ui/headless/_tests/useCommunityMembership.spec.tsx +179 -0
  68. package/src/ui/headless/_tests/useCommunityModeration.spec.tsx +420 -0
  69. package/src/ui/headless/_tests/useCommunityReads.spec.tsx +295 -0
  70. package/src/ui/headless/_tests/useCommunityUpgrade.spec.tsx +157 -0
  71. package/src/ui/headless/_tests/useCommunityWiki.spec.tsx +224 -0
  72. package/src/ui/headless/_tests/useSiteApp.spec.tsx +52 -0
  73. package/src/ui/headless/_tests/wikiMediaImage.spec.tsx +132 -0
  74. package/src/ui/headless/auth/useEmailCodeSignIn.ts +186 -0
  75. package/src/ui/headless/auth/useSignupForm.ts +30 -3
  76. package/src/ui/headless/chat/useChatAttachmentUpload.ts +9 -6
  77. package/src/ui/headless/chat/useChatRoom.ts +35 -1
  78. package/src/ui/headless/communities/CommunityGate.tsx +67 -0
  79. package/src/ui/headless/communities/index.ts +96 -0
  80. package/src/ui/headless/communities/paths.ts +36 -0
  81. package/src/ui/headless/communities/useCommunity.ts +140 -0
  82. package/src/ui/headless/communities/useCommunityAbout.ts +71 -0
  83. package/src/ui/headless/communities/useCommunityCalendar.ts +156 -0
  84. package/src/ui/headless/communities/useCommunityClassroom.ts +84 -0
  85. package/src/ui/headless/communities/useCommunityHome.ts +91 -0
  86. package/src/ui/headless/communities/useCommunityInvite.ts +116 -0
  87. package/src/ui/headless/communities/useCommunityJoin.ts +494 -0
  88. package/src/ui/headless/communities/useCommunityMembership.ts +182 -0
  89. package/src/ui/headless/communities/useCommunityUpgrade.ts +213 -0
  90. package/src/ui/headless/communities/useCommunityWiki.ts +266 -0
  91. package/src/ui/headless/communities/useMyMemberships.ts +59 -0
  92. package/src/ui/headless/community/communityPoll.ts +297 -0
  93. package/src/ui/headless/community/countries.ts +123 -0
  94. package/src/ui/headless/community/index.ts +81 -1
  95. package/src/ui/headless/community/linkPreview.ts +71 -0
  96. package/src/ui/headless/community/richLexical.ts +27 -2
  97. package/src/ui/headless/community/useCommunityAgent.ts +285 -0
  98. package/src/ui/headless/community/useCommunityAttachmentUpload.ts +7 -3
  99. package/src/ui/headless/community/useCommunityMembers.ts +175 -0
  100. package/src/ui/headless/community/useCommunityModeration.ts +300 -0
  101. package/src/ui/headless/community/useCommunitySpaceFeed.ts +22 -4
  102. package/src/ui/headless/community/useCommunityThread.ts +16 -3
  103. package/src/ui/headless/community/useMentionComposer.ts +17 -9
  104. package/src/ui/headless/community/useRichMentionComposer.ts +21 -5
  105. package/src/ui/headless/downloads/index.ts +8 -0
  106. package/src/ui/headless/downloads/useDownloadPage.ts +134 -0
  107. package/src/ui/headless/index.ts +21 -4
  108. package/src/ui/headless/memberHome/index.ts +11 -1
  109. package/src/ui/headless/memberHome/memberHomeBar.ts +91 -0
  110. package/src/ui/headless/memberHome/useMemberHomeNav.ts +84 -44
  111. package/src/ui/headless/membership/_tests/membershipCheckoutRefetch.spec.tsx +8 -0
  112. package/src/ui/headless/membership/_tests/useMembershipCheckoutApplication.spec.tsx +5 -4
  113. package/src/ui/headless/membership/_tests/useMembershipCheckoutQuestionnaire.spec.tsx +1 -1
  114. package/src/ui/headless/membership/_tests/useMembershipCheckoutTrial.spec.tsx +1 -1
  115. package/src/ui/headless/membership/useMembershipCheckout.ts +89 -22
  116. package/src/ui/headless/membership/useMembershipGateNotice.ts +25 -7
  117. package/src/ui/headless/portal/index.ts +57 -0
  118. package/src/ui/headless/portal/usePortal.ts +274 -0
  119. package/src/ui/headless/portal/usePortalFiles.ts +68 -0
  120. package/src/ui/headless/portal/usePortalMessages.ts +189 -0
  121. package/src/ui/headless/portal/usePortalPreview.ts +142 -0
  122. package/src/ui/headless/posts/upsell.ts +54 -8
  123. package/src/ui/headless/posts/useMemberPostsFeed.ts +6 -5
  124. package/src/ui/headless/posts/usePostCollectionDetail.ts +7 -4
  125. package/src/ui/headless/posts/usePostCollectionsList.ts +6 -5
  126. package/src/ui/headless/posts/usePostItem.ts +6 -5
  127. package/src/ui/headless/siteApp/appBar.ts +102 -0
  128. package/src/ui/headless/siteApp/appHome.ts +344 -0
  129. package/src/ui/headless/siteApp/appPrompts.ts +229 -0
  130. package/src/ui/headless/siteApp/index.ts +58 -0
  131. package/src/ui/headless/siteApp/useAppHome.ts +210 -0
  132. package/src/ui/headless/siteApp/useAppInstallRequest.ts +16 -0
  133. package/src/ui/headless/work/index.ts +2 -0
  134. package/src/ui/headless/work/useWorkPortal.ts +37 -26
  135. package/src/ui/index.ts +28 -12
  136. package/src/ui/styled/AccountDashboard.tsx +754 -602
  137. package/src/ui/styled/AiAgentWidget.tsx +13 -1
  138. package/src/ui/styled/ChatRoom.tsx +126 -16
  139. package/src/ui/styled/Checkout.tsx +8 -0
  140. package/src/ui/styled/CheckoutConfirmation.tsx +8 -0
  141. package/src/ui/styled/CoachingConfirmation.tsx +8 -0
  142. package/src/ui/styled/CoachingDetail.tsx +2 -0
  143. package/src/ui/styled/CourseConfirmation.tsx +5 -0
  144. package/src/ui/styled/CourseDetail.tsx +2 -0
  145. package/src/ui/styled/DownloadPage.tsx +263 -0
  146. package/src/ui/styled/EventConfirmation.tsx +10 -0
  147. package/src/ui/styled/EventDetail.tsx +10 -0
  148. package/src/ui/styled/FilmLibrary.tsx +22 -9
  149. package/src/ui/styled/MembershipCheckout.tsx +37 -17
  150. package/src/ui/styled/MembershipTierCallout.tsx +20 -1
  151. package/src/ui/styled/MembershipTiers.tsx +49 -11
  152. package/src/ui/styled/MusicLinkPage.tsx +5 -5
  153. package/src/ui/styled/Paywall.tsx +1 -1
  154. package/src/ui/styled/ProductDetail.tsx +12 -0
  155. package/src/ui/styled/PwaRegistration.tsx +5 -1
  156. package/src/ui/styled/SignupForm.tsx +18 -3
  157. package/src/ui/styled/_tests/AccountDashboardBookingCall.spec.tsx +6 -6
  158. package/src/ui/styled/_tests/AccountDashboardMemberships.spec.tsx +151 -0
  159. package/src/ui/styled/_tests/AccountDashboardRentals.spec.tsx +1 -1
  160. package/src/ui/styled/_tests/AccountNotifications.spec.tsx +135 -0
  161. package/src/ui/styled/_tests/CourseCallsSection.spec.tsx +7 -1
  162. package/src/ui/styled/_tests/DownloadPage.spec.tsx +68 -0
  163. package/src/ui/styled/_tests/MembershipCheckout.spec.tsx +54 -11
  164. package/src/ui/styled/_tests/membershipTierCallout.spec.tsx +1 -1
  165. package/src/ui/styled/_tests/membershipTiersCuratedAccess.spec.tsx +1 -1
  166. package/src/ui/styled/app/AppBar.tsx +298 -0
  167. package/src/ui/styled/app/AppHome.tsx +1139 -0
  168. package/src/ui/styled/app/AppHomeMark.tsx +66 -0
  169. package/src/ui/styled/app/AppPrompts.tsx +464 -0
  170. package/src/ui/styled/app/_tests/AppBar.spec.tsx +96 -0
  171. package/src/ui/styled/app/_tests/AppHome.spec.tsx +209 -0
  172. package/src/ui/styled/app/_tests/AppPrompts.spec.tsx +232 -0
  173. package/src/ui/styled/app/index.ts +7 -0
  174. package/src/ui/styled/chat/ChatThreadPanel.tsx +29 -4
  175. package/src/ui/styled/chat/MessageBubble.tsx +43 -6
  176. package/src/ui/styled/communities/CommunitiesList.tsx +88 -0
  177. package/src/ui/styled/communities/CommunityAbout.tsx +145 -0
  178. package/src/ui/styled/communities/CommunityCalendar.tsx +484 -0
  179. package/src/ui/styled/communities/CommunityClassroom.tsx +302 -0
  180. package/src/ui/styled/communities/CommunityHome.tsx +298 -0
  181. package/src/ui/styled/communities/CommunityJoinSheet.tsx +544 -0
  182. package/src/ui/styled/communities/CommunityMembershipMenu.tsx +249 -0
  183. package/src/ui/styled/communities/CommunityPending.tsx +49 -0
  184. package/src/ui/styled/communities/CommunityPublicWiki.tsx +75 -0
  185. package/src/ui/styled/communities/CommunityUpgradeSheet.tsx +137 -0
  186. package/src/ui/styled/communities/CommunityWiki.tsx +289 -0
  187. package/src/ui/styled/communities/MyMembershipsList.tsx +115 -0
  188. package/src/ui/styled/communities/_tests/CommunityAppHome.spec.tsx +92 -0
  189. package/src/ui/styled/communities/_tests/CommunityClassroomCalendar.spec.tsx +453 -0
  190. package/src/ui/styled/communities/_tests/CommunityMobile.spec.tsx +399 -0
  191. package/src/ui/styled/communities/_tests/CommunityPages.spec.tsx +102 -0
  192. package/src/ui/styled/communities/_tests/CommunityWiki.spec.tsx +223 -0
  193. package/src/ui/styled/communities/index.ts +19 -0
  194. package/src/ui/styled/communities/shared.tsx +177 -0
  195. package/src/ui/styled/community/CommunityAgent.tsx +286 -0
  196. package/src/ui/styled/community/CommunityComposer.tsx +266 -15
  197. package/src/ui/styled/community/CommunityFeed.tsx +50 -1
  198. package/src/ui/styled/community/CommunityLinkPreview.tsx +159 -0
  199. package/src/ui/styled/community/CommunityMemberGlobe.tsx +171 -0
  200. package/src/ui/styled/community/CommunityMemberProfile.tsx +137 -4
  201. package/src/ui/styled/community/CommunityMembers.tsx +387 -0
  202. package/src/ui/styled/community/CommunityModeration.tsx +587 -0
  203. package/src/ui/styled/community/CommunityModerationState.tsx +127 -0
  204. package/src/ui/styled/community/CommunityModeratorMenu.tsx +317 -0
  205. package/src/ui/styled/community/CommunityNotifications.tsx +52 -6
  206. package/src/ui/styled/community/CommunityPollCard.tsx +250 -0
  207. package/src/ui/styled/community/CommunityPostCard.tsx +110 -34
  208. package/src/ui/styled/community/CommunityPostDetail.tsx +157 -71
  209. package/src/ui/styled/community/CommunityRichText.tsx +177 -32
  210. package/src/ui/styled/community/CommunitySpaces.tsx +49 -14
  211. package/src/ui/styled/community/_tests/CommunityAgent.spec.tsx +571 -0
  212. package/src/ui/styled/community/_tests/CommunityComposer.spec.tsx +4 -1
  213. package/src/ui/styled/community/_tests/CommunityLinkPreview.spec.tsx +167 -0
  214. package/src/ui/styled/community/_tests/CommunityMembers.spec.tsx +137 -0
  215. package/src/ui/styled/community/_tests/CommunityModeration.spec.tsx +291 -0
  216. package/src/ui/styled/community/_tests/CommunityPoll.spec.tsx +386 -0
  217. package/src/ui/styled/community/_tests/CommunityPostDetail.spec.tsx +4 -1
  218. package/src/ui/styled/community/index.ts +37 -2
  219. package/src/ui/styled/forge-utilities.css +253 -0
  220. package/src/ui/styled/members/CallRecordingActions.tsx +182 -0
  221. package/src/ui/styled/members/CallTranscriptViewer.tsx +424 -0
  222. package/src/ui/styled/members/MemberCallsCalendar.tsx +41 -54
  223. package/src/ui/styled/members/MemberHomeLayout.tsx +327 -29
  224. package/src/ui/styled/members/MemberPostCard.tsx +9 -5
  225. package/src/ui/styled/members/MemberPostsFeed.tsx +3 -2
  226. package/src/ui/styled/members/PostCollectionDetail.tsx +18 -10
  227. package/src/ui/styled/members/PostCollectionsGrid.tsx +24 -10
  228. package/src/ui/styled/members/_tests/MemberCallsCalendar.spec.tsx +167 -8
  229. package/src/ui/styled/members/callTranscriptText.ts +67 -0
  230. package/src/ui/styled/members/index.ts +8 -0
  231. package/src/ui/styled/portal/PortalAccountSection.tsx +61 -0
  232. package/src/ui/styled/portal/PortalActivity.tsx +167 -0
  233. package/src/ui/styled/portal/PortalBilling.tsx +197 -0
  234. package/src/ui/styled/portal/PortalFiles.tsx +279 -0
  235. package/src/ui/styled/portal/PortalHome.tsx +401 -0
  236. package/src/ui/styled/portal/PortalMessages.tsx +300 -0
  237. package/src/ui/styled/portal/PortalPreviewBanner.tsx +75 -0
  238. package/src/ui/styled/portal/PortalProjects.tsx +407 -0
  239. package/src/ui/styled/portal/PortalShell.tsx +397 -0
  240. package/src/ui/styled/portal/_tests/PortalAppHome.spec.tsx +90 -0
  241. package/src/ui/styled/portal/_tests/PortalFiles.spec.tsx +159 -0
  242. package/src/ui/styled/portal/_tests/PortalHome.spec.tsx +148 -0
  243. package/src/ui/styled/portal/_tests/PortalMessages.spec.tsx +168 -0
  244. package/src/ui/styled/portal/_tests/PortalPreviewBanner.spec.tsx +132 -0
  245. package/src/ui/styled/portal/_tests/portalNav.spec.ts +123 -0
  246. package/src/ui/styled/portal/index.ts +13 -0
  247. package/src/ui/styled/portal/portalNav.ts +260 -0
  248. package/src/ui/styled/portal/shared.tsx +206 -0
  249. package/src/ui/styled/work/WorkInviteAccept.tsx +228 -168
  250. package/src/ui/styled/work/WorkPortalProjects.tsx +68 -120
  251. package/src/ui/styled/work/WorkProjectDocuments.tsx +99 -128
  252. package/src/ui/styled/work/WorkProjectInvoices.tsx +91 -86
  253. package/src/ui/styled/work/WorkProjectReport.tsx +11 -17
  254. package/src/ui/styled/work/WorkReportBody.tsx +223 -248
  255. package/src/ui/styled/work/WorkTaskAttachments.tsx +23 -29
  256. package/src/ui/styled/work/WorkTaskComments.tsx +75 -102
  257. package/src/ui/styled/work/WorkTaskDetail.tsx +48 -59
  258. package/src/ui/styled/work/WorkTokenReport.tsx +8 -16
  259. package/src/ui/styled/work/index.ts +1 -1
  260. package/src/utils/_tests/metaEvents.spec.ts +264 -0
  261. package/src/utils/_tests/safeRedirect.spec.ts +2 -2
  262. package/src/utils/landing.ts +19 -6
  263. package/src/utils/metaEvents.ts +302 -0
  264. package/src/ui/headless/membership/MembershipGate.tsx +0 -44
  265. package/src/ui/styled/_tests/AccountDashboardCommunity.spec.tsx +0 -149
  266. package/src/ui/styled/_tests/AccountDashboardTrial.spec.tsx +0 -105
@@ -1,13 +1,16 @@
1
- import { CheckCircle2, ChevronRight, Circle, Clock, Flag } from "lucide-react";
1
+ import type { CSSProperties, MouseEvent, ReactNode } from "react";
2
+ import { CheckCircle2, ChevronRight, Circle } from "lucide-react";
3
+ import { useForgeT, type ForgeT } from "../../../i18n";
2
4
  import { useThemeTokens, type ResolvedThemeTokens } from "../../theme/ForgeThemeProvider";
3
- import {
4
- formatWorkDate,
5
- formatWorkMoney,
6
- type WorkProjectReportData,
7
- } from "../../headless/work/useWorkPortal";
5
+ import { formatWorkMoney, type WorkProjectReportData } from "../../headless/work/useWorkPortal";
6
+ import { PortalBlock, PortalHeading, PortalList, PortalRow, usePortal } from "../portal/shared";
7
+
8
+ export type WorkReportSection = "header" | "milestones" | "tasks" | "time";
9
+
10
+ const ALL_SECTIONS: WorkReportSection[] = ["header", "milestones", "tasks", "time"];
8
11
 
9
12
  /**
10
- * Shared read-only renderer for a project's report — used by both the authed
13
+ * Shared read-only renderer for a project's report, used by both the authed
11
14
  * `WorkProjectReport` and the public tokenized `WorkTokenReport`. Shows
12
15
  * milestones, a simple task list, and the billable-only time report
13
16
  * (hours + amount per task + total). Theme tokens only.
@@ -18,275 +21,245 @@ export interface WorkReportBodyProps {
18
21
  * report only), each task row becomes a link. The public token report omits
19
22
  * this, so tasks stay read-only there. */
20
23
  taskHref?: (taskId: string) => string;
21
- /** Router-driven open handler. When provided, task rows render as buttons
22
- * calling this instead of anchors. Takes precedence over `taskHref`. */
24
+ /** Router-driven open handler. Without `taskHref`, task rows render as
25
+ * buttons calling this. With both, rows are real links whose plain left
26
+ * click calls this instead of reloading the page. */
23
27
  onOpenTask?: (taskId: string) => void;
28
+ /** Which parts to render, in this fixed order. Defaults to all four, which
29
+ * is what the public token report shows. */
30
+ sections?: WorkReportSection[];
31
+ }
32
+
33
+ /** Project and milestone status in words. Literal keys, so the translation spec can see each one. */
34
+ export function workStatusLabel(tr: ForgeT, status: string): string {
35
+ const labels: Record<string, string> = {
36
+ active: tr("forge.work_report_body.status_active"),
37
+ on_hold: tr("forge.work_report_body.status_on_hold"),
38
+ completed: tr("forge.work_report_body.status_completed"),
39
+ archived: tr("forge.work_report_body.status_archived"),
40
+ planning: tr("forge.work_report_body.status_planning"),
41
+ pending: tr("forge.work_report_body.status_pending"),
42
+ in_progress: tr("forge.work_report_body.status_in_progress"),
43
+ };
44
+ return labels[status] ?? status.replace(/_/g, " ");
24
45
  }
25
46
 
26
- export function WorkReportBody({ report, taskHref, onOpenTask }: WorkReportBodyProps) {
47
+ const isComplete = (status: string) => ["done", "completed", "complete"].includes(status?.toLowerCase());
48
+
49
+ export function WorkReportBody({ report, taskHref, onOpenTask, sections = ALL_SECTIONS }: WorkReportBodyProps) {
27
50
  const t = useThemeTokens();
51
+ const tr = useForgeT();
52
+ const { formatDate } = usePortal();
28
53
  const { project, milestones, tasks, timeReport } = report;
29
54
  const taskOpenable = !!taskHref || !!onOpenTask;
55
+ const show = (section: WorkReportSection) => sections.includes(section);
30
56
 
31
57
  return (
32
- <div
33
- data-testid="work-project-report"
34
- style={{ display: "flex", flexDirection: "column", gap: 24, color: t.text, fontFamily: t.fontFamily }}
35
- >
36
- {/* Header */}
37
- <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", flexWrap: "wrap", gap: 8 }}>
38
- <h1 data-testid="work-project-name" style={{ fontSize: 22, fontWeight: 800, margin: 0, color: t.text }}>
39
- {project.name}
40
- </h1>
41
- <StatusPill status={project.status} t={t} />
42
- </div>
43
-
44
- {/* Milestones */}
45
- <Panel title="Milestones" icon={<Flag size={16} />} t={t}>
46
- {milestones.length === 0 ? (
47
- <Muted t={t}>No milestones yet.</Muted>
48
- ) : (
49
- <ul style={listStyle}>
50
- {milestones.map((m) => (
51
- <li key={m.id} style={rowStyle}>
52
- <span style={{ display: "inline-flex", alignItems: "center", gap: 8 }}>
53
- <StatusDot status={m.status} t={t} />
54
- <span style={{ color: t.text }}>{m.name}</span>
55
- </span>
56
- <span style={{ fontSize: 13, color: t.muted }}>{formatWorkDate(m.dueDate)}</span>
57
- </li>
58
- ))}
59
- </ul>
60
- )}
61
- </Panel>
58
+ <div data-testid="work-project-report" style={{ color: t.text, fontFamily: t.fontFamily }}>
59
+ {show("header") && (
60
+ <PortalHeading
61
+ title={<span data-testid="work-project-name">{project.name}</span>}
62
+ actions={
63
+ <span data-testid="work-project-status" style={{ fontSize: 14, color: t.muted }}>
64
+ {workStatusLabel(tr, project.status)}
65
+ </span>
66
+ }
67
+ />
68
+ )}
62
69
 
63
- {/* Task list */}
64
- <Panel title="Tasks" icon={<CheckCircle2 size={16} />} t={t}>
65
- {tasks.length === 0 ? (
66
- <Muted t={t}>No tasks yet.</Muted>
67
- ) : (
68
- <ul style={listStyle}>
69
- {tasks.map((task) => {
70
- const inner = (
71
- <>
72
- <span style={{ display: "inline-flex", alignItems: "center", gap: 8 }}>
73
- {task.done ? (
74
- <CheckCircle2 size={16} style={{ color: t.primary }} />
75
- ) : (
76
- <Circle size={16} style={{ color: t.muted }} />
77
- )}
78
- <span
79
- style={{
80
- color: task.done ? t.muted : t.text,
81
- textDecoration: task.done ? "line-through" : "none",
82
- }}
83
- >
84
- {task.title}
70
+ {show("milestones") && (
71
+ <PortalBlock title={tr("forge.work_report_body.milestones")}>
72
+ {milestones.length === 0 ? (
73
+ <Muted t={t}>{tr("forge.work_report_body.milestones_empty")}</Muted>
74
+ ) : (
75
+ <PortalList>
76
+ {milestones.map((m) => {
77
+ const complete = isComplete(m.status);
78
+ return (
79
+ <PortalRow key={m.id} testId="work-milestone">
80
+ <span className="fg:min-w-0 fg:flex-1" style={{ fontWeight: 600, color: complete ? t.muted : t.text }}>
81
+ {m.name}
85
82
  </span>
86
- </span>
87
- <span style={{ display: "inline-flex", alignItems: "center", gap: 8 }}>
88
- <span style={{ fontSize: 13, color: t.muted }}>{formatWorkDate(task.dueDate)}</span>
89
- {taskOpenable && <ChevronRight size={16} style={{ color: t.muted, flexShrink: 0 }} />}
90
- </span>
91
- </>
92
- );
83
+ <span className="fg:flex fg:flex-wrap fg:gap-x-4" style={{ fontSize: 13, color: t.muted }}>
84
+ {m.dueDate ? <span>{tr("forge.work_report_body.due_on", { date: formatDate(m.dueDate) })}</span> : null}
85
+ <span>{workStatusLabel(tr, m.status)}</span>
86
+ </span>
87
+ </PortalRow>
88
+ );
89
+ })}
90
+ </PortalList>
91
+ )}
92
+ </PortalBlock>
93
+ )}
93
94
 
94
- if (!taskOpenable) {
95
- return (
96
- <li key={task.id} style={rowStyle}>
97
- {inner}
98
- </li>
95
+ {show("tasks") && (
96
+ <PortalBlock title={tr("forge.work_report_body.tasks")}>
97
+ {tasks.length === 0 ? (
98
+ <Muted t={t}>{tr("forge.work_report_body.tasks_empty")}</Muted>
99
+ ) : (
100
+ <PortalList>
101
+ {tasks.map((task) => {
102
+ const inner = (
103
+ <>
104
+ <span className="fg:flex fg:min-w-0 fg:flex-1 fg:items-center fg:gap-3">
105
+ {task.done ? (
106
+ <CheckCircle2 size={18} aria-hidden style={{ color: t.text, flexShrink: 0 }} />
107
+ ) : (
108
+ <Circle size={18} aria-hidden style={{ color: t.muted, flexShrink: 0 }} />
109
+ )}
110
+ <span className="fg:sr-only">
111
+ {task.done ? tr("forge.work_report_body.task_done") : tr("forge.work_report_body.task_open")}
112
+ </span>
113
+ <span
114
+ style={{
115
+ color: task.done ? t.muted : t.text,
116
+ textDecoration: task.done ? "line-through" : "none",
117
+ }}
118
+ >
119
+ {task.title}
120
+ </span>
121
+ </span>
122
+ <span className="fg:flex fg:items-center fg:gap-3" style={{ fontSize: 13, color: t.muted }}>
123
+ {task.dueDate ? (
124
+ <span>{tr("forge.work_report_body.due_on", { date: formatDate(task.dueDate) })}</span>
125
+ ) : null}
126
+ {taskOpenable && <ChevronRight size={16} aria-hidden style={{ flexShrink: 0 }} />}
127
+ </span>
128
+ </>
99
129
  );
100
- }
101
130
 
102
- const linkStyle: React.CSSProperties = {
103
- ...rowStyle,
104
- width: "100%",
105
- textAlign: "left",
106
- cursor: "pointer",
107
- background: "transparent",
108
- border: "none",
109
- padding: "4px 0",
110
- color: t.text,
111
- fontFamily: t.fontFamily,
112
- fontSize: "inherit",
113
- textDecoration: "none",
114
- };
131
+ if (!taskOpenable) {
132
+ return (
133
+ <PortalRow key={task.id} testId="work-task-row">
134
+ {inner}
135
+ </PortalRow>
136
+ );
137
+ }
138
+
139
+ const linkStyle: CSSProperties = {
140
+ width: "100%",
141
+ textAlign: "left",
142
+ cursor: "pointer",
143
+ background: "transparent",
144
+ border: "none",
145
+ color: t.text,
146
+ fontFamily: t.fontFamily,
147
+ fontSize: "inherit",
148
+ textDecoration: "none",
149
+ };
150
+ const linkClass = "fg:flex fg:flex-wrap fg:items-center fg:gap-x-4 fg:gap-y-2 fg:px-0 fg:py-4";
115
151
 
116
- if (onOpenTask) {
117
152
  return (
118
- <li key={task.id} style={{ listStyle: "none" }}>
119
- <button type="button" data-testid="work-task-link" onClick={() => onOpenTask(task.id)} style={linkStyle}>
120
- {inner}
121
- </button>
153
+ <li key={task.id} style={{ listStyle: "none", borderBottom: `1px solid ${t.border}` }}>
154
+ {taskHref ? (
155
+ <a
156
+ data-testid="work-task-link"
157
+ href={taskHref(task.id)}
158
+ className={linkClass}
159
+ style={linkStyle}
160
+ onClick={
161
+ onOpenTask
162
+ ? (event: MouseEvent<HTMLAnchorElement>) => {
163
+ if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
164
+ return;
165
+ }
166
+ event.preventDefault();
167
+ onOpenTask(task.id);
168
+ }
169
+ : undefined
170
+ }
171
+ >
172
+ {inner}
173
+ </a>
174
+ ) : (
175
+ <button
176
+ type="button"
177
+ data-testid="work-task-link"
178
+ onClick={() => onOpenTask!(task.id)}
179
+ className={linkClass}
180
+ style={linkStyle}
181
+ >
182
+ {inner}
183
+ </button>
184
+ )}
122
185
  </li>
123
186
  );
124
- }
125
-
126
- return (
127
- <li key={task.id} style={{ listStyle: "none" }}>
128
- <a data-testid="work-task-link" href={taskHref!(task.id)} style={linkStyle}>
129
- {inner}
130
- </a>
131
- </li>
132
- );
133
- })}
134
- </ul>
135
- )}
136
- </Panel>
187
+ })}
188
+ </PortalList>
189
+ )}
190
+ </PortalBlock>
191
+ )}
137
192
 
138
- {/* Billable time report */}
139
- <Panel title="Billable time" icon={<Clock size={16} />} t={t}>
140
- {timeReport.byTask.length === 0 ? (
141
- <Muted t={t}>No billable time logged yet.</Muted>
142
- ) : (
143
- <div data-testid="work-time-report" style={{ width: "100%", overflowX: "auto" }}>
144
- <table style={{ width: "100%", borderCollapse: "collapse", fontSize: 14 }}>
145
- <thead>
146
- <tr>
147
- <Th align="left" t={t}>
148
- Task
149
- </Th>
150
- <Th align="right" t={t}>
151
- Hours
152
- </Th>
153
- <Th align="right" t={t}>
154
- Amount
155
- </Th>
156
- </tr>
157
- </thead>
158
- <tbody>
159
- {timeReport.byTask.map((row) => (
160
- <tr key={row.taskId ?? "unassigned"}>
161
- <Td align="left" t={t}>
162
- {row.taskTitle}
193
+ {show("time") && (
194
+ <PortalBlock title={tr("forge.work_report_body.billable_time")}>
195
+ {timeReport.byTask.length === 0 ? (
196
+ <Muted t={t}>{tr("forge.work_report_body.billable_time_empty")}</Muted>
197
+ ) : (
198
+ <div data-testid="work-time-report" style={{ width: "100%", overflowX: "auto" }}>
199
+ <table style={{ width: "100%", borderCollapse: "collapse", fontSize: 15 }}>
200
+ <thead>
201
+ <tr>
202
+ <Th align="left" t={t}>
203
+ {tr("forge.work_report_body.column_task")}
204
+ </Th>
205
+ <Th align="right" t={t}>
206
+ {tr("forge.work_report_body.column_hours")}
207
+ </Th>
208
+ <Th align="right" t={t}>
209
+ {tr("forge.work_report_body.column_amount")}
210
+ </Th>
211
+ </tr>
212
+ </thead>
213
+ <tbody>
214
+ {timeReport.byTask.map((row) => (
215
+ <tr key={row.taskId ?? "unassigned"}>
216
+ <Td align="left" t={t}>
217
+ {row.taskTitle}
218
+ </Td>
219
+ <Td align="right" t={t}>
220
+ {row.hours.toLocaleString()}
221
+ </Td>
222
+ <Td align="right" t={t}>
223
+ {formatWorkMoney(row.amount, timeReport.currency)}
224
+ </Td>
225
+ </tr>
226
+ ))}
227
+ </tbody>
228
+ <tfoot>
229
+ <tr data-testid="work-time-total">
230
+ <Td align="left" bold t={t}>
231
+ {tr("forge.work_report_body.total")}
163
232
  </Td>
164
- <Td align="right" t={t}>
165
- {row.hours.toLocaleString()}
233
+ <Td align="right" bold t={t}>
234
+ {timeReport.totalHours.toLocaleString()}
166
235
  </Td>
167
- <Td align="right" t={t}>
168
- {formatWorkMoney(row.amount, timeReport.currency)}
236
+ <Td align="right" bold t={t} testId="work-total-amount">
237
+ {formatWorkMoney(timeReport.totalAmount, timeReport.currency)}
169
238
  </Td>
170
239
  </tr>
171
- ))}
172
- </tbody>
173
- <tfoot>
174
- <tr data-testid="work-time-total">
175
- <Td align="left" bold t={t}>
176
- Total
177
- </Td>
178
- <Td align="right" bold t={t}>
179
- {timeReport.totalHours.toLocaleString()}
180
- </Td>
181
- <Td align="right" bold t={t} testId="work-total-amount">
182
- {formatWorkMoney(timeReport.totalAmount, timeReport.currency)}
183
- </Td>
184
- </tr>
185
- </tfoot>
186
- </table>
187
- </div>
188
- )}
189
- </Panel>
240
+ </tfoot>
241
+ </table>
242
+ </div>
243
+ )}
244
+ </PortalBlock>
245
+ )}
190
246
  </div>
191
247
  );
192
248
  }
193
249
 
194
- const listStyle: React.CSSProperties = {
195
- margin: 0,
196
- padding: 0,
197
- listStyle: "none",
198
- display: "flex",
199
- flexDirection: "column",
200
- gap: 8,
201
- };
202
-
203
- const rowStyle: React.CSSProperties = {
204
- display: "flex",
205
- alignItems: "center",
206
- justifyContent: "space-between",
207
- gap: 12,
208
- flexWrap: "wrap",
209
- };
210
-
211
- function Panel({
212
- title,
213
- icon,
214
- t,
215
- children,
216
- }: {
217
- title: string;
218
- icon?: React.ReactNode;
219
- t: ResolvedThemeTokens;
220
- children: React.ReactNode;
221
- }) {
222
- return (
223
- <div style={{ padding: 20, border: `1px solid ${t.border}`, borderRadius: t.cornerRadius, background: t.surface }}>
224
- <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 12 }}>
225
- {icon && <span style={{ color: t.primary, display: "inline-flex" }}>{icon}</span>}
226
- <h2
227
- style={{
228
- fontSize: 13,
229
- fontWeight: 600,
230
- textTransform: "uppercase",
231
- letterSpacing: "0.05em",
232
- margin: 0,
233
- color: t.text,
234
- }}
235
- >
236
- {title}
237
- </h2>
238
- </div>
239
- {children}
240
- </div>
241
- );
242
- }
243
-
244
- function Muted({ t, children }: { t: ResolvedThemeTokens; children: React.ReactNode }) {
250
+ function Muted({ t, children }: { t: ResolvedThemeTokens; children: ReactNode }) {
245
251
  return <p style={{ margin: 0, fontSize: 14, color: t.muted }}>{children}</p>;
246
252
  }
247
253
 
248
- function StatusPill({ status, t }: { status: string; t: ResolvedThemeTokens }) {
249
- return (
250
- <span
251
- style={{
252
- fontSize: 12,
253
- fontWeight: 600,
254
- padding: "4px 10px",
255
- borderRadius: 999,
256
- textTransform: "capitalize",
257
- background: t.border,
258
- color: t.primary,
259
- }}
260
- >
261
- {status.replace(/_/g, " ")}
262
- </span>
263
- );
264
- }
265
-
266
- function StatusDot({ status, t }: { status: string; t: ResolvedThemeTokens }) {
267
- const done = ["done", "completed", "complete"].includes(status?.toLowerCase());
268
- return (
269
- <span
270
- style={{
271
- display: "inline-block",
272
- width: 8,
273
- height: 8,
274
- borderRadius: "50%",
275
- background: done ? t.primary : t.muted,
276
- }}
277
- />
278
- );
279
- }
280
-
281
- function Th({ align, t, children }: { align: "left" | "right"; t: ResolvedThemeTokens; children: React.ReactNode }) {
254
+ function Th({ align, t, children }: { align: "left" | "right"; t: ResolvedThemeTokens; children: ReactNode }) {
282
255
  return (
283
256
  <th
257
+ scope="col"
284
258
  style={{
285
- padding: "8px",
286
- fontWeight: 600,
287
- fontSize: 12,
288
- textTransform: "uppercase",
289
- letterSpacing: "0.05em",
259
+ padding: "0 0 10px",
260
+ paddingLeft: align === "right" ? 16 : 0,
261
+ fontWeight: 500,
262
+ fontSize: 13,
290
263
  textAlign: align,
291
264
  color: t.muted,
292
265
  borderBottom: `1px solid ${t.border}`,
@@ -308,17 +281,19 @@ function Td({
308
281
  bold?: boolean;
309
282
  t: ResolvedThemeTokens;
310
283
  testId?: string;
311
- children: React.ReactNode;
284
+ children: ReactNode;
312
285
  }) {
313
286
  return (
314
287
  <td
315
288
  data-testid={testId}
316
289
  style={{
317
- padding: "8px",
290
+ padding: "12px 0",
291
+ paddingLeft: align === "right" ? 16 : 0,
318
292
  textAlign: align,
319
293
  color: t.text,
320
- fontWeight: bold ? 700 : 400,
321
- borderTop: `1px solid ${t.border}`,
294
+ fontWeight: bold ? 600 : 400,
295
+ fontVariantNumeric: "tabular-nums",
296
+ borderBottom: bold ? "none" : `1px solid ${t.border}`,
322
297
  }}
323
298
  >
324
299
  {children}
@@ -1,6 +1,8 @@
1
1
  import { Paperclip } from "lucide-react";
2
+ import { useForgeT } from "../../../i18n";
2
3
  import { useThemeTokens } from "../../theme/ForgeThemeProvider";
3
4
  import { useWorkTaskAttachments } from "../../headless/work/useWorkPortal";
5
+ import { PortalBlock, PortalList, PortalRow } from "../portal/shared";
4
6
 
5
7
  export interface WorkTaskAttachmentsProps {
6
8
  /** The client project id. */
@@ -27,41 +29,33 @@ const humanSize = (size: number | null): string => {
27
29
  */
28
30
  export function WorkTaskAttachments({ projectId, taskId }: WorkTaskAttachmentsProps) {
29
31
  const t = useThemeTokens();
32
+ const tr = useForgeT();
30
33
  const { data: attachments, isLoading } = useWorkTaskAttachments(projectId, taskId);
31
34
 
32
35
  if (isLoading) return null;
33
36
  if (!attachments || attachments.length === 0) return null;
34
37
 
35
38
  return (
36
- <div data-testid="work-task-attachments" style={{ display: "flex", flexDirection: "column", gap: 10 }}>
37
- <span style={{ fontSize: 13, fontWeight: 700, color: t.text }}>Attachments</span>
38
- <ul style={{ listStyle: "none", margin: 0, padding: 0, display: "flex", flexDirection: "column", gap: 8 }}>
39
- {attachments.map((att) => (
40
- <li key={att.id} data-testid={`work-task-attachment-${att.id}`}>
41
- <a
42
- href={att.url ?? "#"}
43
- target="_blank"
44
- rel="noreferrer"
45
- style={{
46
- display: "inline-flex",
47
- alignItems: "center",
48
- gap: 8,
49
- fontSize: 13,
50
- color: t.primary,
51
- textDecoration: "none",
52
- padding: "8px 12px",
53
- border: `1px solid ${t.border}`,
54
- borderRadius: t.cornerRadius,
55
- background: t.surface,
56
- }}
57
- >
58
- <Paperclip size={14} />
59
- <span>{att.name || att.filename || "Attachment"}</span>
60
- {att.size ? <span style={{ color: t.muted }}>({humanSize(att.size)})</span> : null}
61
- </a>
62
- </li>
63
- ))}
64
- </ul>
39
+ <div data-testid="work-task-attachments" style={{ color: t.text, fontFamily: t.fontFamily }}>
40
+ <PortalBlock title={tr("forge.work_task_attachments.title")}>
41
+ <PortalList>
42
+ {attachments.map((att) => (
43
+ <PortalRow key={att.id} testId={`work-task-attachment-${att.id}`}>
44
+ <Paperclip size={16} aria-hidden style={{ color: t.muted, flexShrink: 0 }} />
45
+ <a
46
+ href={att.url ?? "#"}
47
+ target="_blank"
48
+ rel="noreferrer"
49
+ className="fg:min-w-0 fg:flex-1"
50
+ style={{ color: t.text, fontWeight: 600, fontSize: 15, textDecoration: "underline", textUnderlineOffset: 3 }}
51
+ >
52
+ {att.name || att.filename || tr("forge.work_task_attachments.fallback_name")}
53
+ </a>
54
+ {att.size ? <span style={{ fontSize: 13, color: t.muted }}>{humanSize(att.size)}</span> : null}
55
+ </PortalRow>
56
+ ))}
57
+ </PortalList>
58
+ </PortalBlock>
65
59
  </div>
66
60
  );
67
61
  }