@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,12 +1,10 @@
1
- import { useState, type FormEvent } from "react";
2
- import { MessageSquare, Send } from "lucide-react";
3
- import { useThemeTokens, type ResolvedThemeTokens } from "../../theme/ForgeThemeProvider";
1
+ import { useId, useState, type FormEvent } from "react";
2
+ import { Send } from "lucide-react";
3
+ import { useForgeT } from "../../../i18n";
4
+ import { useThemeTokens } from "../../theme/ForgeThemeProvider";
4
5
  import { Loading } from "../Loading";
5
- import {
6
- formatWorkDate,
7
- useWorkTaskComments,
8
- type WorkTaskComment,
9
- } from "../../headless/work/useWorkPortal";
6
+ import { useWorkTaskComments, type WorkTaskComment } from "../../headless/work/useWorkPortal";
7
+ import { PortalBlock, usePortal, usePortalButtons } from "../portal/shared";
10
8
 
11
9
  export interface WorkTaskCommentsProps {
12
10
  /** The client project id (from the route). */
@@ -16,13 +14,15 @@ export interface WorkTaskCommentsProps {
16
14
  }
17
15
 
18
16
  /**
19
- * The authenticated client's comment thread for a single task — renders the
20
- * existing comments (author + time + content, newest first) and a composer
21
- * (textarea + submit) that posts a new one. Built on `useWorkTaskComments`;
22
- * theme tokens only.
17
+ * The authenticated client's comment thread for a single task. Renders the
18
+ * existing comments (author, time and content, newest first) and a composer
19
+ * that posts a new one. Built on `useWorkTaskComments`; theme tokens only.
23
20
  */
24
21
  export function WorkTaskComments({ projectId, taskId }: WorkTaskCommentsProps) {
25
22
  const t = useThemeTokens();
23
+ const tr = useForgeT();
24
+ const buttons = usePortalButtons();
25
+ const inputId = useId();
26
26
  const { comments, isLoading, error, addComment, isAdding } = useWorkTaskComments(projectId, taskId);
27
27
  const [content, setContent] = useState("");
28
28
 
@@ -38,112 +38,85 @@ export function WorkTaskComments({ projectId, taskId }: WorkTaskCommentsProps) {
38
38
  }
39
39
  };
40
40
 
41
- return (
42
- <div
43
- data-testid="work-task-comments"
44
- style={{ display: "flex", flexDirection: "column", gap: 16, color: t.text, fontFamily: t.fontFamily }}
45
- >
46
- <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
47
- <MessageSquare size={16} style={{ color: t.primary }} />
48
- <h2
49
- style={{
50
- fontSize: 13,
51
- fontWeight: 600,
52
- textTransform: "uppercase",
53
- letterSpacing: "0.05em",
54
- margin: 0,
55
- color: t.text,
56
- }}
57
- >
58
- Comments
59
- </h2>
60
- </div>
41
+ const empty = content.trim().length === 0;
61
42
 
62
- {isLoading ? (
63
- <Loading />
64
- ) : error ? (
65
- <p style={{ margin: 0, fontSize: 14, color: t.muted }}>We couldn&apos;t load comments right now.</p>
66
- ) : comments.length === 0 ? (
67
- <p style={{ margin: 0, fontSize: 14, color: t.muted }}>No comments yet. Start the conversation below.</p>
68
- ) : (
69
- <ul style={{ margin: 0, padding: 0, listStyle: "none", display: "flex", flexDirection: "column", gap: 12 }}>
70
- {comments.map((c) => (
71
- <CommentItem key={c.id} comment={c} t={t} />
72
- ))}
73
- </ul>
74
- )}
43
+ return (
44
+ <div data-testid="work-task-comments" style={{ color: t.text, fontFamily: t.fontFamily }}>
45
+ <PortalBlock title={tr("forge.work_task_comments.title")}>
46
+ {isLoading ? (
47
+ <Loading />
48
+ ) : error ? (
49
+ <p style={{ margin: 0, fontSize: 14, color: t.muted }}>{tr("forge.work_task_comments.load_error")}</p>
50
+ ) : comments.length === 0 ? (
51
+ <p style={{ margin: 0, fontSize: 14, color: t.muted }}>{tr("forge.work_task_comments.empty")}</p>
52
+ ) : (
53
+ <ul style={{ margin: 0, padding: 0, listStyle: "none", borderTop: `1px solid ${t.border}` }}>
54
+ {comments.map((c) => (
55
+ <CommentItem key={c.id} comment={c} />
56
+ ))}
57
+ </ul>
58
+ )}
75
59
 
76
- {/* Composer */}
77
- <form onSubmit={handleSubmit} style={{ display: "flex", flexDirection: "column", gap: 8 }}>
78
- <textarea
79
- data-testid="work-task-comment-input"
80
- value={content}
81
- onChange={(e) => setContent(e.target.value)}
82
- placeholder="Add a comment…"
83
- rows={3}
84
- disabled={isAdding}
85
- style={{
86
- width: "100%",
87
- resize: "vertical",
88
- padding: "10px 12px",
89
- fontFamily: t.fontFamily,
90
- fontSize: 14,
91
- color: t.text,
92
- background: t.background,
93
- border: `1px solid ${t.border}`,
94
- borderRadius: t.cornerRadius,
95
- boxSizing: "border-box",
96
- }}
97
- />
98
- <div style={{ display: "flex", justifyContent: "flex-end" }}>
99
- <button
100
- type="submit"
101
- data-testid="work-task-comment-submit"
102
- disabled={isAdding || content.trim().length === 0}
60
+ <form onSubmit={handleSubmit} className="fg:mt-6 fg:flex fg:flex-col fg:gap-3">
61
+ <label htmlFor={inputId} className="fg:sr-only">
62
+ {tr("forge.work_task_comments.input_label")}
63
+ </label>
64
+ <textarea
65
+ id={inputId}
66
+ data-testid="work-task-comment-input"
67
+ value={content}
68
+ onChange={(e) => setContent(e.target.value)}
69
+ placeholder={tr("forge.work_task_comments.placeholder")}
70
+ rows={3}
71
+ disabled={isAdding}
103
72
  style={{
104
- display: "inline-flex",
105
- alignItems: "center",
106
- gap: 8,
107
- padding: "8px 16px",
73
+ width: "100%",
74
+ resize: "vertical",
75
+ padding: "10px 12px",
108
76
  fontFamily: t.fontFamily,
109
- fontSize: 14,
110
- fontWeight: 600,
111
- color: t.textPrimary,
112
- background: t.primary,
113
- border: "none",
77
+ fontSize: 15,
78
+ color: t.text,
79
+ background: t.background,
80
+ border: `1px solid ${t.border}`,
114
81
  borderRadius: t.cornerRadius,
115
- cursor: isAdding || content.trim().length === 0 ? "not-allowed" : "pointer",
116
- opacity: isAdding || content.trim().length === 0 ? 0.6 : 1,
82
+ boxSizing: "border-box",
117
83
  }}
118
- >
119
- <Send size={14} />
120
- {isAdding ? "Posting…" : "Post comment"}
121
- </button>
122
- </div>
123
- </form>
84
+ />
85
+ <div className="fg:flex fg:justify-end">
86
+ <button
87
+ type="submit"
88
+ data-testid="work-task-comment-submit"
89
+ disabled={isAdding || empty}
90
+ style={{
91
+ ...buttons.primary,
92
+ cursor: isAdding || empty ? "not-allowed" : "pointer",
93
+ opacity: isAdding || empty ? 0.5 : 1,
94
+ }}
95
+ >
96
+ <Send size={14} aria-hidden />
97
+ {isAdding ? tr("forge.work_task_comments.posting") : tr("forge.work_task_comments.submit")}
98
+ </button>
99
+ </div>
100
+ </form>
101
+ </PortalBlock>
124
102
  </div>
125
103
  );
126
104
  }
127
105
 
128
- function CommentItem({ comment, t }: { comment: WorkTaskComment; t: ResolvedThemeTokens }) {
106
+ function CommentItem({ comment }: { comment: WorkTaskComment }) {
107
+ const t = useThemeTokens();
108
+ const { formatDate } = usePortal();
129
109
  return (
130
110
  <li
131
111
  data-testid="work-task-comment-item"
132
- style={{
133
- padding: 14,
134
- border: `1px solid ${t.border}`,
135
- borderRadius: t.cornerRadius,
136
- background: t.surface,
137
- display: "flex",
138
- flexDirection: "column",
139
- gap: 6,
140
- }}
112
+ className="fg:flex fg:flex-col fg:gap-1.5 fg:py-4"
113
+ style={{ borderBottom: `1px solid ${t.border}` }}
141
114
  >
142
- <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8, flexWrap: "wrap" }}>
115
+ <div className="fg:flex fg:flex-wrap fg:items-baseline fg:gap-x-3">
143
116
  <span style={{ fontWeight: 600, fontSize: 14, color: t.text }}>{comment.fullName}</span>
144
- <span style={{ fontSize: 12, color: t.muted }}>{formatWorkDate(comment.createdAt)}</span>
117
+ <span style={{ fontSize: 13, color: t.muted }}>{formatDate(comment.createdAt)}</span>
145
118
  </div>
146
- <p style={{ margin: 0, fontSize: 14, color: t.text, whiteSpace: "pre-wrap", wordBreak: "break-word" }}>
119
+ <p style={{ margin: 0, fontSize: 15, lineHeight: 1.5, color: t.text, whiteSpace: "pre-wrap", wordBreak: "break-word" }}>
147
120
  {comment.content}
148
121
  </p>
149
122
  </li>
@@ -1,7 +1,11 @@
1
- import { ArrowLeft, CheckCircle2, Circle } from "lucide-react";
1
+ import type { MouseEvent } from "react";
2
+ import { ArrowLeft } from "lucide-react";
3
+ import { useForgeT } from "../../../i18n";
4
+ import { internalPath } from "../../../runtime/pages";
2
5
  import { useThemeTokens } from "../../theme/ForgeThemeProvider";
3
6
  import { Loading } from "../Loading";
4
- import { formatWorkDate, useWorkProjectReport } from "../../headless/work/useWorkPortal";
7
+ import { useWorkProjectReport } from "../../headless/work/useWorkPortal";
8
+ import { PortalHeading, usePortal } from "../portal/shared";
5
9
  import { WorkTaskComments } from "./WorkTaskComments";
6
10
  import { WorkTaskAttachments } from "./WorkTaskAttachments";
7
11
 
@@ -10,81 +14,66 @@ export interface WorkTaskDetailProps {
10
14
  projectId: string;
11
15
  /** The task id (from the route). */
12
16
  taskId: string;
13
- /** Href back to the project report. Rendered as an `<a>` unless `onBack` is
14
- * supplied. Defaults to `/i/work/projects/{projectId}`. */
17
+ /** Href back to the project report. Defaults to
18
+ * `internalPath("account/projects/$projectId")`. */
15
19
  backHref?: string;
16
- /** Router-driven back handler. When provided, the back link renders as a
17
- * button calling this instead of an anchor. */
20
+ /** Router-driven back handler. The back link stays a real `<a href>`, and a
21
+ * plain left click calls this instead of reloading the page. */
18
22
  onBack?: () => void;
19
23
  }
20
24
 
21
25
  /**
22
- * The authenticated client's single-task detail — the task title/status plus
23
- * the comment thread (`WorkTaskComments`). Resolves the task from the project
24
- * report so it shows a friendly title. Theme tokens only.
26
+ * The authenticated client's single-task detail: the task title and status,
27
+ * its attachments and the comment thread (`WorkTaskComments`). Resolves the
28
+ * task from the project report so it shows a friendly title. Theme tokens only.
25
29
  */
26
30
  export function WorkTaskDetail({ projectId, taskId, backHref, onBack }: WorkTaskDetailProps) {
27
31
  const t = useThemeTokens();
32
+ const tr = useForgeT();
33
+ const { formatDate } = usePortal();
28
34
  const { data: report, isLoading } = useWorkProjectReport(projectId);
29
- const href = backHref ?? `/i/work/projects/${projectId}`;
35
+ const href = backHref ?? internalPath("account/projects/$projectId", { projectId });
30
36
 
31
37
  const task = report?.tasks.find((tk) => tk.id === taskId);
32
38
 
33
- const backLinkStyle: React.CSSProperties = {
34
- display: "inline-flex",
35
- alignItems: "center",
36
- gap: 6,
37
- fontSize: 13,
38
- fontWeight: 600,
39
- color: t.primary,
40
- background: "transparent",
41
- border: "none",
42
- padding: 0,
43
- cursor: "pointer",
44
- textDecoration: "none",
45
- };
46
-
47
- const backLink = onBack ? (
48
- <button type="button" data-testid="work-task-back" onClick={onBack} style={backLinkStyle}>
49
- <ArrowLeft size={14} /> Back to project
50
- </button>
51
- ) : (
52
- <a data-testid="work-task-back" href={href} style={backLinkStyle}>
53
- <ArrowLeft size={14} /> Back to project
54
- </a>
55
- );
56
-
57
39
  return (
58
- <div
59
- data-testid="work-task-detail"
60
- style={{ display: "flex", flexDirection: "column", gap: 24, color: t.text, fontFamily: t.fontFamily }}
61
- >
62
- {backLink}
40
+ <div data-testid="work-task-detail" style={{ color: t.text, fontFamily: t.fontFamily }}>
41
+ <a
42
+ data-testid="work-task-back"
43
+ href={href}
44
+ className="fg:mb-4 fg:inline-flex fg:items-center fg:gap-1.5"
45
+ style={{ fontSize: 14, color: t.muted, textDecoration: "none" }}
46
+ onClick={
47
+ onBack
48
+ ? (event: MouseEvent<HTMLAnchorElement>) => {
49
+ if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
50
+ event.preventDefault();
51
+ onBack();
52
+ }
53
+ : undefined
54
+ }
55
+ >
56
+ <ArrowLeft size={14} aria-hidden /> {tr("forge.work_task_detail.back_to_project")}
57
+ </a>
63
58
 
64
59
  {isLoading ? (
65
- <Loading fullPage />
60
+ <Loading />
66
61
  ) : (
67
- <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12, flexWrap: "wrap" }}>
68
- <h1
69
- data-testid="work-task-title"
70
- style={{ fontSize: 22, fontWeight: 800, margin: 0, color: t.text, display: "inline-flex", alignItems: "center", gap: 10 }}
71
- >
72
- {task ? (
73
- task.done ? (
74
- <CheckCircle2 size={20} style={{ color: t.primary }} />
75
- ) : (
76
- <Circle size={20} style={{ color: t.muted }} />
77
- )
78
- ) : null}
79
- {task ? task.title : "Task"}
80
- </h1>
81
- {task?.dueDate && <span style={{ fontSize: 13, color: t.muted }}>Due {formatWorkDate(task.dueDate)}</span>}
82
- </div>
62
+ <PortalHeading
63
+ title={<span data-testid="work-task-title">{task ? task.title : tr("forge.work_task_detail.fallback_title")}</span>}
64
+ lead={task?.dueDate ? tr("forge.work_task_detail.due_on", { date: formatDate(task.dueDate) }) : undefined}
65
+ actions={
66
+ task ? (
67
+ <span data-testid="work-task-status" style={{ fontSize: 14, color: t.muted }}>
68
+ {task.done ? tr("forge.work_task_detail.status_done") : tr("forge.work_task_detail.status_open")}
69
+ </span>
70
+ ) : undefined
71
+ }
72
+ />
83
73
  )}
84
74
 
85
- <WorkTaskAttachments projectId={projectId} taskId={taskId} />
86
-
87
- <div style={{ padding: 20, border: `1px solid ${t.border}`, borderRadius: t.cornerRadius, background: t.surface }}>
75
+ <div className="fg:flex fg:flex-col fg:gap-10">
76
+ <WorkTaskAttachments projectId={projectId} taskId={taskId} />
88
77
  <WorkTaskComments projectId={projectId} taskId={taskId} />
89
78
  </div>
90
79
  </div>
@@ -1,21 +1,24 @@
1
+ import { useForgeT } from "../../../i18n";
1
2
  import { useThemeTokens } from "../../theme/ForgeThemeProvider";
2
3
  import { Loading } from "../Loading";
3
4
  import { useWorkTokenReport } from "../../headless/work/useWorkPortal";
5
+ import { PortalEmpty } from "../portal/shared";
4
6
  import { WorkReportBody } from "./WorkReportBody";
5
7
 
6
8
  export interface WorkTokenReportProps {
7
- /** Signed share token from the URL (`/i/work/time-report/:token`). */
9
+ /** Signed share token from the URL (`/i/account/time-report/:token`). */
8
10
  token: string;
9
11
  }
10
12
 
11
13
  /**
12
14
  * Public, login-free tokenized share of a project's time report. The signed
13
- * token IS the authorization — no auth guard. Invalid/expired tokens resolve to
14
- * a friendly not-found notice (the API returns 404). Built on
15
+ * token IS the authorization, so there is no auth guard. Invalid or expired
16
+ * tokens resolve to a friendly not-found notice (the API returns 404). Built on
15
17
  * `useWorkTokenReport`; wraps `WorkReportBody`. Theme tokens only.
16
18
  */
17
19
  export function WorkTokenReport({ token }: WorkTokenReportProps) {
18
20
  const t = useThemeTokens();
21
+ const tr = useForgeT();
19
22
  const { data: report, isLoading, isError } = useWorkTokenReport(token);
20
23
 
21
24
  if (isLoading) {
@@ -24,19 +27,8 @@ export function WorkTokenReport({ token }: WorkTokenReportProps) {
24
27
 
25
28
  if (isError || !report) {
26
29
  return (
27
- <div
28
- data-testid="work-token-report-error"
29
- style={{
30
- border: `1px solid ${t.border}`,
31
- borderRadius: t.cornerRadius,
32
- background: t.surface,
33
- padding: 32,
34
- textAlign: "center",
35
- color: t.text,
36
- fontFamily: t.fontFamily,
37
- }}
38
- >
39
- <p style={{ margin: 0, color: t.muted }}>This report link is invalid or has expired.</p>
30
+ <div data-testid="work-token-report-error" style={{ fontFamily: t.fontFamily }}>
31
+ <PortalEmpty title={tr("forge.work_token_report.invalid_link")} />
40
32
  </div>
41
33
  );
42
34
  }
@@ -2,7 +2,7 @@
2
2
  export { WorkPortalProjects, type WorkPortalProjectsProps } from "./WorkPortalProjects";
3
3
  export { WorkProjectReport, type WorkProjectReportProps } from "./WorkProjectReport";
4
4
  export { WorkTokenReport, type WorkTokenReportProps } from "./WorkTokenReport";
5
- export { WorkReportBody, type WorkReportBodyProps } from "./WorkReportBody";
5
+ export { WorkReportBody, type WorkReportBodyProps, type WorkReportSection } from "./WorkReportBody";
6
6
  export { WorkTaskComments, type WorkTaskCommentsProps } from "./WorkTaskComments";
7
7
  export { WorkTaskAttachments, type WorkTaskAttachmentsProps } from "./WorkTaskAttachments";
8
8
  export { WorkTaskDetail, type WorkTaskDetailProps } from "./WorkTaskDetail";