hale-commenting-system 2.2.0 → 2.2.2

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 (99) hide show
  1. package/.claude/settings.local.json +7 -0
  2. package/.editorconfig +17 -0
  3. package/.eslintrc.js +75 -0
  4. package/.github/ISSUE_TEMPLATE/bug_report.md +23 -0
  5. package/.github/workflows/ci.yaml +51 -0
  6. package/.prettierignore +1 -0
  7. package/.prettierrc +4 -0
  8. package/GITHUB_OAUTH_ENV_TEMPLATE.md +53 -0
  9. package/LICENSE +21 -0
  10. package/README.md +92 -21
  11. package/package.json +74 -50
  12. package/scripts/README.md +42 -0
  13. package/scripts/integrate.js +472 -0
  14. package/src/app/AppLayout/AppLayout.tsx +248 -0
  15. package/src/app/Comments/Comments.tsx +273 -0
  16. package/src/app/Dashboard/Dashboard.tsx +10 -0
  17. package/src/app/NotFound/NotFound.tsx +35 -0
  18. package/src/app/Settings/General/GeneralSettings.tsx +16 -0
  19. package/src/app/Settings/Profile/ProfileSettings.tsx +18 -0
  20. package/src/app/Support/Support.tsx +50 -0
  21. package/src/app/__snapshots__/app.test.tsx.snap +524 -0
  22. package/src/app/app.css +11 -0
  23. package/src/app/app.test.tsx +55 -0
  24. package/src/app/bgimages/Patternfly-Logo.svg +28 -0
  25. package/src/app/commenting-system/components/CommentOverlay.tsx +93 -0
  26. package/src/app/commenting-system/components/CommentPanel.tsx +534 -0
  27. package/src/app/commenting-system/components/CommentPin.tsx +60 -0
  28. package/src/app/commenting-system/components/DetailsTab.tsx +516 -0
  29. package/src/app/commenting-system/components/FloatingWidget.tsx +130 -0
  30. package/src/app/commenting-system/components/JiraTab.tsx +696 -0
  31. package/src/app/commenting-system/contexts/CommentContext.tsx +1033 -0
  32. package/src/app/commenting-system/contexts/GitHubAuthContext.tsx +84 -0
  33. package/{dist/index.d.ts → src/app/commenting-system/index.ts} +5 -4
  34. package/src/app/commenting-system/services/githubAdapter.ts +359 -0
  35. package/src/app/commenting-system/types/index.ts +27 -0
  36. package/src/app/commenting-system/utils/version.ts +19 -0
  37. package/src/app/index.tsx +22 -0
  38. package/src/app/routes.tsx +81 -0
  39. package/src/app/utils/useDocumentTitle.ts +13 -0
  40. package/src/favicon.png +0 -0
  41. package/src/index.html +18 -0
  42. package/src/index.tsx +25 -0
  43. package/src/test/setup.ts +33 -0
  44. package/src/typings.d.ts +12 -0
  45. package/stylePaths.js +14 -0
  46. package/tsconfig.json +34 -0
  47. package/vitest.config.ts +19 -0
  48. package/webpack.common.js +139 -0
  49. package/webpack.dev.js +318 -0
  50. package/webpack.prod.js +38 -0
  51. package/bin/detect.d.ts +0 -10
  52. package/bin/detect.js +0 -134
  53. package/bin/generators.d.ts +0 -20
  54. package/bin/generators.js +0 -272
  55. package/bin/hale-commenting.js +0 -4
  56. package/bin/index.d.ts +0 -2
  57. package/bin/index.js +0 -61
  58. package/bin/onboarding.d.ts +0 -1
  59. package/bin/onboarding.js +0 -395
  60. package/bin/postinstall.d.ts +0 -2
  61. package/bin/postinstall.js +0 -65
  62. package/bin/validators.d.ts +0 -2
  63. package/bin/validators.js +0 -66
  64. package/dist/cli/detect.d.ts +0 -10
  65. package/dist/cli/detect.js +0 -134
  66. package/dist/cli/generators.d.ts +0 -20
  67. package/dist/cli/generators.js +0 -272
  68. package/dist/cli/index.d.ts +0 -2
  69. package/dist/cli/index.js +0 -61
  70. package/dist/cli/onboarding.d.ts +0 -1
  71. package/dist/cli/onboarding.js +0 -395
  72. package/dist/cli/postinstall.d.ts +0 -2
  73. package/dist/cli/postinstall.js +0 -65
  74. package/dist/cli/validators.d.ts +0 -2
  75. package/dist/cli/validators.js +0 -66
  76. package/dist/components/CommentOverlay.d.ts +0 -2
  77. package/dist/components/CommentOverlay.js +0 -101
  78. package/dist/components/CommentPanel.d.ts +0 -6
  79. package/dist/components/CommentPanel.js +0 -334
  80. package/dist/components/CommentPin.d.ts +0 -11
  81. package/dist/components/CommentPin.js +0 -64
  82. package/dist/components/DetailsTab.d.ts +0 -2
  83. package/dist/components/DetailsTab.js +0 -380
  84. package/dist/components/FloatingWidget.d.ts +0 -8
  85. package/dist/components/FloatingWidget.js +0 -128
  86. package/dist/components/JiraTab.d.ts +0 -2
  87. package/dist/components/JiraTab.js +0 -507
  88. package/dist/contexts/CommentContext.d.ts +0 -30
  89. package/dist/contexts/CommentContext.js +0 -891
  90. package/dist/contexts/GitHubAuthContext.d.ts +0 -13
  91. package/dist/contexts/GitHubAuthContext.js +0 -96
  92. package/dist/index.js +0 -27
  93. package/dist/services/githubAdapter.d.ts +0 -56
  94. package/dist/services/githubAdapter.js +0 -321
  95. package/dist/types/index.d.ts +0 -25
  96. package/dist/types/index.js +0 -2
  97. package/dist/utils/version.d.ts +0 -1
  98. package/dist/utils/version.js +0 -23
  99. package/templates/webpack-middleware.js +0 -226
@@ -0,0 +1,248 @@
1
+ import * as React from 'react';
2
+ import { NavLink, useLocation } from 'react-router-dom';
3
+ import {
4
+ Button,
5
+ Masthead,
6
+ MastheadBrand,
7
+ MastheadLogo,
8
+ MastheadMain,
9
+ MastheadToggle,
10
+ Nav,
11
+ NavExpandable,
12
+ NavItem,
13
+ NavList,
14
+ Page,
15
+ PageSidebar,
16
+ PageSidebarBody,
17
+ SkipToContent,
18
+ Switch,
19
+ } from '@patternfly/react-core';
20
+ import { IAppRoute, IAppRouteGroup, routes } from '@app/routes';
21
+ import { BarsIcon, ExternalLinkAltIcon, GithubIcon } from '@patternfly/react-icons';
22
+ import { CommentOverlay, CommentPanel, useComments, useGitHubAuth } from '@app/commenting-system';
23
+
24
+ interface IAppLayout {
25
+ children: React.ReactNode;
26
+ }
27
+
28
+ const AppLayout: React.FunctionComponent<IAppLayout> = ({ children }) => {
29
+ const [sidebarOpen, setSidebarOpen] = React.useState(true);
30
+ const { commentsEnabled, setCommentsEnabled, drawerPinnedOpen, setDrawerPinnedOpen, floatingWidgetMode, setFloatingWidgetMode } = useComments();
31
+ const { isAuthenticated, user, login, logout } = useGitHubAuth();
32
+ const masthead = (
33
+ <Masthead>
34
+ <MastheadMain>
35
+ <MastheadToggle>
36
+ <Button
37
+ icon={<BarsIcon />}
38
+ variant="plain"
39
+ onClick={() => setSidebarOpen(!sidebarOpen)}
40
+ aria-label="Global navigation"
41
+ />
42
+ </MastheadToggle>
43
+ <MastheadBrand data-codemods>
44
+ <MastheadLogo data-codemods>
45
+ <svg height="40px" viewBox="0 0 679 158">
46
+ <title>PatternFly logo</title>
47
+ <defs>
48
+ <linearGradient x1="68%" y1="2.25860997e-13%" x2="32%" y2="100%" id="linearGradient-basic-masthead">
49
+ <stop stopColor="#2B9AF3" offset="0%"></stop>
50
+ <stop stopColor="#73BCF7" stopOpacity="0.502212631" offset="100%"></stop>
51
+ </linearGradient>
52
+ </defs>
53
+ <g stroke="none" strokeWidth="1" fill="none" fillRule="evenodd">
54
+ <g
55
+ transform="translate(206.000000, 45.750000)"
56
+ fill="var(--pf-t--global--text--color--regular)"
57
+ fillRule="nonzero"
58
+ >
59
+ <path d="M0,65.25 L0,2.25 L33.21,2.25 C37.35,2.25 41.025,3.135 44.235,4.905 C47.445,6.675 49.98,9.09 51.84,12.15 C53.7,15.21 54.63,18.72 54.63,22.68 C54.63,26.46 53.7,29.865 51.84,32.895 C49.98,35.925 47.43,38.31 44.19,40.05 C40.95,41.79 37.29,42.66 33.21,42.66 L15.48,42.66 L15.48,65.25 L0,65.25 Z M15.48,29.88 L31.41,29.88 C33.69,29.88 35.52,29.22 36.9,27.9 C38.28,26.58 38.97,24.87 38.97,22.77 C38.97,20.61 38.28,18.855 36.9,17.505 C35.52,16.155 33.69,15.48 31.41,15.48 L15.48,15.48 L15.48,29.88 Z"></path>
60
+ <path d="M77.04,66.06 C73.68,66.06 70.695,65.43 68.085,64.17 C65.475,62.91 63.435,61.17 61.965,58.95 C60.495,56.73 59.76,54.18 59.76,51.3 C59.76,46.74 61.485,43.215 64.935,40.725 C68.385,38.235 73.2,36.99 79.38,36.99 C83.1,36.99 86.7,37.44 90.18,38.34 L90.18,36 C90.18,31.26 87.15,28.89 81.09,28.89 C77.49,28.89 72.69,30.15 66.69,32.67 L61.47,21.96 C69.15,18.48 76.56,16.74 83.7,16.74 C90.3,16.74 95.43,18.315 99.09,21.465 C102.75,24.615 104.58,29.04 104.58,34.74 L104.58,65.25 L90.18,65.25 L90.18,62.37 C88.26,63.69 86.235,64.635 84.105,65.205 C81.975,65.775 79.62,66.06 77.04,66.06 Z M73.62,51.03 C73.62,52.53 74.28,53.7 75.6,54.54 C76.92,55.38 78.75,55.8 81.09,55.8 C84.69,55.8 87.72,55.05 90.18,53.55 L90.18,47.43 C87.42,46.71 84.54,46.35 81.54,46.35 C79.02,46.35 77.07,46.755 75.69,47.565 C74.31,48.375 73.62,49.53 73.62,51.03 Z"></path>
61
+ <path d="M137.25,65.88 C125.73,65.88 119.97,60.84 119.97,50.76 L119.97,29.79 L110.34,29.79 L110.34,17.64 L119.97,17.64 L119.97,5.4 L134.55,2.25 L134.55,17.64 L147.87,17.64 L147.87,29.79 L134.55,29.79 L134.55,47.88 C134.55,49.98 135.015,51.465 135.945,52.335 C136.875,53.205 138.51,53.64 140.85,53.64 C143.01,53.64 145.2,53.31 147.42,52.65 L147.42,64.44 C146.1,64.86 144.42,65.205 142.38,65.475 C140.34,65.745 138.63,65.88 137.25,65.88 Z"></path>
62
+ <path d="M177.57,65.88 C166.05,65.88 160.29,60.84 160.29,50.76 L160.29,29.79 L150.66,29.79 L150.66,17.64 L160.29,17.64 L160.29,5.4 L174.87,2.25 L174.87,17.64 L188.19,17.64 L188.19,29.79 L174.87,29.79 L174.87,47.88 C174.87,49.98 175.335,51.465 176.265,52.335 C177.195,53.205 178.83,53.64 181.17,53.64 C183.33,53.64 185.52,53.31 187.74,52.65 L187.74,64.44 C186.42,64.86 184.74,65.205 182.7,65.475 C180.66,65.745 178.95,65.88 177.57,65.88 Z"></path>
63
+ <path d="M217.62,66.15 C212.76,66.15 208.365,65.055 204.435,62.865 C200.505,60.675 197.4,57.72 195.12,54 C192.84,50.28 191.7,46.11 191.7,41.49 C191.7,36.87 192.795,32.7 194.985,28.98 C197.175,25.26 200.16,22.305 203.94,20.115 C207.72,17.925 211.92,16.83 216.54,16.83 C221.22,16.83 225.36,17.955 228.96,20.205 C232.56,22.455 235.395,25.53 237.465,29.43 C239.535,33.33 240.57,37.8 240.57,42.84 L240.57,46.44 L206.64,46.44 C207.6,48.66 209.1,50.475 211.14,51.885 C213.18,53.295 215.58,54 218.34,54 C222.42,54 225.6,52.8 227.88,50.4 L237.51,58.95 C234.51,61.47 231.435,63.3 228.285,64.44 C225.135,65.58 221.58,66.15 217.62,66.15 Z M206.37,36.27 L226.26,36.27 C225.48,33.99 224.205,32.16 222.435,30.78 C220.665,29.4 218.61,28.71 216.27,28.71 C213.87,28.71 211.8,29.37 210.06,30.69 C208.32,32.01 207.09,33.87 206.37,36.27 Z"></path>
64
+ <path d="M247.41,65.25 L247.41,17.64 L261.99,17.64 L261.99,22.41 C265.23,18.51 269.4,16.56 274.5,16.56 C277.08,16.62 278.91,17.01 279.99,17.73 L279.99,30.42 C277.95,29.46 275.64,28.98 273.06,28.98 C270.78,28.98 268.665,29.505 266.715,30.555 C264.765,31.605 263.19,33.09 261.99,35.01 L261.99,65.25 L247.41,65.25 Z"></path>
65
+ <path d="M286.29,65.25 L286.29,17.64 L300.87,17.64 L300.87,20.88 C304.47,18.12 308.73,16.74 313.65,16.74 C317.37,16.74 320.655,17.55 323.505,19.17 C326.355,20.79 328.59,23.04 330.21,25.92 C331.83,28.8 332.64,32.13 332.64,35.91 L332.64,65.25 L318.06,65.25 L318.06,37.89 C318.06,35.25 317.28,33.15 315.72,31.59 C314.16,30.03 312.06,29.25 309.42,29.25 C305.76,29.25 302.91,30.51 300.87,33.03 L300.87,65.25 L286.29,65.25 Z"></path>
66
+ <polygon points="342 65.25 342 2.25 392.04 2.25 392.04 15.66 357.48 15.66 357.48 27.45 380.52 27.45 380.52 40.41 357.48 40.41 357.48 65.25"></polygon>
67
+ <polygon points="399.96 65.25 399.96 2.25 414.54 0 414.54 65.25"></polygon>
68
+ <path d="M429.21,84.69 C428.07,84.69 426.96,84.645 425.88,84.555 C424.8,84.465 423.9,84.33 423.18,84.15 L423.18,71.73 C424.38,71.97 425.88,72.09 427.68,72.09 C432.36,72.09 435.51,70.05 437.13,65.97 L437.13,65.88 L418.86,17.64 L434.97,17.64 L445.5,47.61 L457.74,17.64 L473.49,17.64 L452.16,67.68 C450.42,71.82 448.5,75.135 446.4,77.625 C444.3,80.115 441.87,81.915 439.11,83.025 C436.35,84.135 433.05,84.69 429.21,84.69 Z"></path>
69
+ </g>
70
+ <g transform="translate(0.000000, 0.000000)">
71
+ <path
72
+ d="M61.826087,0 L158,0 L158,96.173913 L147.695652,96.173913 C100.271201,96.173913 61.826087,57.7287992 61.826087,10.3043478 L61.826087,0 L61.826087,0 Z"
73
+ fill="#0066CC"
74
+ ></path>
75
+ <path
76
+ d="M158,3.43478261 L65.2608696,158 L138,158 C149.045695,158 158,149.045695 158,138 L158,3.43478261 L158,3.43478261 Z"
77
+ fill="url(#linearGradient-basic-masthead)"
78
+ ></path>
79
+ <path
80
+ d="M123.652174,-30.9130435 L30.9130435,123.652174 L103.652174,123.652174 C114.697869,123.652174 123.652174,114.697869 123.652174,103.652174 L123.652174,-30.9130435 L123.652174,-30.9130435 Z"
81
+ fill="url(#linearGradient-basic-masthead)"
82
+ transform="translate(77.282609, 46.369565) scale(1, -1) rotate(90.000000) translate(-77.282609, -46.369565) "
83
+ ></path>
84
+ </g>
85
+ </g>
86
+ </svg>
87
+ </MastheadLogo>
88
+ </MastheadBrand>
89
+ </MastheadMain>
90
+ </Masthead>
91
+ );
92
+
93
+ const location = useLocation();
94
+
95
+ const renderNavItem = (route: IAppRoute, index: number) => (
96
+ <NavItem key={`${route.label}-${index}`} id={`${route.label}-${index}`} isActive={route.path === location.pathname}>
97
+ <NavLink
98
+ to={route.path}
99
+ >
100
+ {route.label}
101
+ </NavLink>
102
+ </NavItem>
103
+ );
104
+
105
+ const renderNavGroup = (group: IAppRouteGroup, groupIndex: number) => {
106
+ // Special handling for Comments group
107
+ if (group.label === 'Comments') {
108
+ return (
109
+ <NavExpandable
110
+ key={`${group.label}-${groupIndex}`}
111
+ id={`${group.label}-${groupIndex}`}
112
+ title="Hale Commenting System"
113
+ isActive={group.routes.some((route) => route.path === location.pathname)}
114
+ >
115
+ <NavItem
116
+ onClick={(e) => {
117
+ e.stopPropagation();
118
+ setFloatingWidgetMode(!floatingWidgetMode);
119
+ if (!floatingWidgetMode) {
120
+ setDrawerPinnedOpen(false); // Close drawer if opening floating widget
121
+ }
122
+ }}
123
+ style={{ cursor: 'pointer' }}
124
+ >
125
+ <div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
126
+ <ExternalLinkAltIcon />
127
+ <span>{floatingWidgetMode ? 'Close widget' : 'Pop out'}</span>
128
+ </div>
129
+ </NavItem>
130
+ <NavItem>
131
+ <div
132
+ data-comment-controls
133
+ style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', paddingRight: '1rem' }}
134
+ >
135
+ <span>Enable Comments</span>
136
+ <Switch
137
+ id="comments-enabled-switch"
138
+ isChecked={commentsEnabled}
139
+ onChange={(_event, checked) => {
140
+ setCommentsEnabled(checked);
141
+ if (checked) {
142
+ setDrawerPinnedOpen(true);
143
+ }
144
+ }}
145
+ aria-label="Enable or disable comments"
146
+ />
147
+ </div>
148
+ </NavItem>
149
+ <NavItem>
150
+ <div
151
+ data-comment-controls
152
+ style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', paddingRight: '1rem' }}
153
+ >
154
+ <span>Page info drawer</span>
155
+ <Switch
156
+ id="page-info-drawer-switch"
157
+ isChecked={drawerPinnedOpen}
158
+ onChange={(_event, checked) => setDrawerPinnedOpen(checked)}
159
+ aria-label="Pin page info drawer open"
160
+ />
161
+ </div>
162
+ </NavItem>
163
+ <NavItem>
164
+ <div
165
+ data-comment-controls
166
+ style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', paddingRight: '1rem' }}
167
+ >
168
+ {isAuthenticated ? (
169
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
170
+ <span style={{ display: 'inline-flex', alignItems: 'center', gap: '6px' }}>
171
+ <GithubIcon />
172
+ {user?.login ? `@${user.login}` : 'Signed in'}
173
+ </span>
174
+ <Button variant="link" isInline onClick={logout}>
175
+ Sign out
176
+ </Button>
177
+ </div>
178
+ ) : (
179
+ <Button variant="link" isInline icon={<GithubIcon />} onClick={login}>
180
+ Sign in with GitHub
181
+ </Button>
182
+ )}
183
+ </div>
184
+ </NavItem>
185
+ {group.routes.map((route, idx) => route.label && renderNavItem(route, idx))}
186
+ </NavExpandable>
187
+ );
188
+ }
189
+
190
+ // Default handling for other groups
191
+ return (
192
+ <NavExpandable
193
+ key={`${group.label}-${groupIndex}`}
194
+ id={`${group.label}-${groupIndex}`}
195
+ title={group.label}
196
+ isActive={group.routes.some((route) => route.path === location.pathname)}
197
+ >
198
+ {group.routes.map((route, idx) => route.label && renderNavItem(route, idx))}
199
+ </NavExpandable>
200
+ );
201
+ };
202
+
203
+ const Navigation = (
204
+ <Nav id="nav-primary-simple">
205
+ <NavList id="nav-list-simple">
206
+ {routes.map(
207
+ (route, idx) => route.label && (!route.routes ? renderNavItem(route, idx) : renderNavGroup(route, idx)),
208
+ )}
209
+ </NavList>
210
+ </Nav>
211
+ );
212
+
213
+ const Sidebar = (
214
+ <PageSidebar>
215
+ <PageSidebarBody>{Navigation}</PageSidebarBody>
216
+ </PageSidebar>
217
+ );
218
+
219
+ const pageId = 'primary-app-container';
220
+
221
+ const PageSkipToContent = (
222
+ <SkipToContent
223
+ onClick={(event) => {
224
+ event.preventDefault();
225
+ const primaryContentContainer = document.getElementById(pageId);
226
+ primaryContentContainer?.focus();
227
+ }}
228
+ href={`#${pageId}`}
229
+ >
230
+ Skip to Content
231
+ </SkipToContent>
232
+ );
233
+ return (
234
+ <Page
235
+ mainContainerId={pageId}
236
+ masthead={masthead}
237
+ sidebar={sidebarOpen && Sidebar}
238
+ skipToContent={PageSkipToContent}
239
+ >
240
+ <CommentPanel>
241
+ <CommentOverlay />
242
+ {children}
243
+ </CommentPanel>
244
+ </Page>
245
+ );
246
+ };
247
+
248
+ export { AppLayout };
@@ -0,0 +1,273 @@
1
+ import * as React from 'react';
2
+ import { useNavigate } from 'react-router-dom';
3
+ import {
4
+ Button,
5
+ Card,
6
+ CardBody,
7
+ InputGroup,
8
+ InputGroupItem,
9
+ InputGroupText,
10
+ PageSection,
11
+ TextInput,
12
+ Title,
13
+ } from '@patternfly/react-core';
14
+ import { AngleDownIcon, AngleRightIcon, SearchIcon } from '@patternfly/react-icons';
15
+ import { useComments } from '@app/commenting-system';
16
+
17
+ const Comments: React.FunctionComponent = () => {
18
+ const navigate = useNavigate();
19
+ const { threads, setSelectedThreadId, setCommentsEnabled, setDrawerPinnedOpen } = useComments();
20
+ const [filter, setFilter] = React.useState('');
21
+ const [expanded, setExpanded] = React.useState<Record<string, boolean>>({});
22
+
23
+ // Dummy data so designers can see the intended table layout before any real comments exist.
24
+ const dummyThreads = React.useMemo(() => {
25
+ const now = Date.now();
26
+ return [
27
+ {
28
+ id: 'demo-thread-support-1',
29
+ route: '/support',
30
+ xPercent: 43.0,
31
+ yPercent: 68.3,
32
+ comments: [
33
+ { id: 'demo-c-1', text: 'The empty state is clear, but I’d prefer the primary CTA to be more prominent.', createdAt: new Date(now - 1000 * 60 * 55).toISOString() },
34
+ { id: 'demo-c-2', text: 'Can we add a short “How it works” blurb above the actions?', createdAt: new Date(now - 1000 * 60 * 48).toISOString() },
35
+ { id: 'demo-c-3', text: 'Spacing feels a little tight on smaller screens.', createdAt: new Date(now - 1000 * 60 * 35).toISOString() },
36
+ { id: 'demo-c-4', text: 'Love the hierarchy. Maybe add an icon to the primary action.', createdAt: new Date(now - 1000 * 60 * 22).toISOString() },
37
+ { id: 'demo-c-5', text: 'Could the secondary actions be collapsed into a kebab menu?', createdAt: new Date(now - 1000 * 60 * 12).toISOString() },
38
+ ],
39
+ },
40
+ {
41
+ id: 'demo-thread-projects-1',
42
+ route: '/projects',
43
+ xPercent: 27.5,
44
+ yPercent: 15.3,
45
+ comments: [
46
+ { id: 'demo-c-6', text: 'Filter dropdown should default to “All projects”.', createdAt: new Date(now - 1000 * 60 * 9).toISOString() },
47
+ ],
48
+ },
49
+ {
50
+ id: 'demo-thread-settings-1',
51
+ route: '/settings/general',
52
+ xPercent: 61.2,
53
+ yPercent: 42.7,
54
+ comments: [],
55
+ },
56
+ ];
57
+ }, []);
58
+
59
+ const isUsingDummyData = threads.length === 0;
60
+ const visibleThreads = isUsingDummyData ? dummyThreads : threads;
61
+
62
+ const normalizedFilter = filter.trim().toLowerCase();
63
+ const filteredThreads = visibleThreads
64
+ .filter((t) => {
65
+ if (!normalizedFilter) return true;
66
+ const haystack = `${t.route} ${t.xPercent} ${t.yPercent} ${(t.comments || []).map((c) => c.text).join(' ')}`.toLowerCase();
67
+ return haystack.includes(normalizedFilter);
68
+ })
69
+ .sort((a, b) => {
70
+ const aLast = a.comments?.[a.comments.length - 1]?.createdAt ?? '';
71
+ const bLast = b.comments?.[b.comments.length - 1]?.createdAt ?? '';
72
+ return bLast.localeCompare(aLast);
73
+ });
74
+
75
+ const toggleExpanded = (threadId: string) => {
76
+ setExpanded((prev) => ({ ...prev, [threadId]: !prev[threadId] }));
77
+ };
78
+
79
+ const goToPin = (threadId: string, route: string) => {
80
+ // Ensure visibility + open drawer on destination route
81
+ setCommentsEnabled(true);
82
+ setDrawerPinnedOpen(true);
83
+ setSelectedThreadId(threadId);
84
+ navigate(route);
85
+ };
86
+
87
+ const formatDate = (isoDate: string): string => {
88
+ if (!isoDate) return '—';
89
+ const date = new Date(isoDate);
90
+ return date.toLocaleString(undefined, {
91
+ month: 'short',
92
+ day: 'numeric',
93
+ hour: '2-digit',
94
+ minute: '2-digit',
95
+ });
96
+ };
97
+
98
+ return (
99
+ // Mark this page as "comment controls" so clicking around doesn't create pins
100
+ <PageSection data-comment-controls>
101
+ <Title headingLevel="h1" size="lg" style={{ marginBottom: 'var(--pf-t--global--spacer--sm)' }}>
102
+ View comments
103
+ </Title>
104
+ <p style={{ marginBottom: 'var(--pf-t--global--spacer--md)', color: 'var(--pf-t--global--text--color--subtle)' }}>
105
+ A <b>thread</b> is created when someone drops a pin on a screen. Expand a row to read comments, or use "Go to
106
+ pin" to jump to the exact spot.
107
+ </p>
108
+ <Card>
109
+ <CardBody>
110
+ {isUsingDummyData && (
111
+ <p
112
+ style={{
113
+ marginBottom: 'var(--pf-t--global--spacer--md)',
114
+ fontSize: 'var(--pf-t--global--font--size--sm)',
115
+ color: 'var(--pf-t--global--text--color--subtle)',
116
+ }}
117
+ >
118
+ Showing <b>sample data</b> (no real comment threads yet). Once pins/comments exist, this table will show
119
+ live data.
120
+ </p>
121
+ )}
122
+ <div style={{ display: 'flex', gap: 'var(--pf-t--global--spacer--sm)', marginBottom: 'var(--pf-t--global--spacer--md)' }}>
123
+ <InputGroup style={{ flex: 1 }}>
124
+ <InputGroupItem>
125
+ <InputGroupText>
126
+ <SearchIcon />
127
+ </InputGroupText>
128
+ </InputGroupItem>
129
+ <InputGroupItem isFill>
130
+ <TextInput
131
+ aria-label="Filter comments"
132
+ value={filter}
133
+ onChange={(_e, v) => setFilter(v)}
134
+ placeholder="Search by page, text, or coordinates…"
135
+ />
136
+ </InputGroupItem>
137
+ </InputGroup>
138
+ <InputGroupItem>
139
+ <Button variant="secondary" onClick={() => setFilter('')} isDisabled={!filter.trim()}>
140
+ Clear
141
+ </Button>
142
+ </InputGroupItem>
143
+ </div>
144
+
145
+ <div style={{ overflowX: 'auto' }}>
146
+ <table className="pf-v6-c-table pf-m-grid-md" role="grid" aria-label="All comment threads table">
147
+ <thead className="pf-v6-c-table__thead">
148
+ <tr className="pf-v6-c-table__tr">
149
+ <th className="pf-v6-c-table__th" scope="col" style={{ width: '3rem' }} />
150
+ <th className="pf-v6-c-table__th" scope="col">
151
+ Screen
152
+ </th>
153
+ <th className="pf-v6-c-table__th" scope="col">
154
+ Pin location
155
+ </th>
156
+ <th className="pf-v6-c-table__th" scope="col">
157
+ Comments
158
+ </th>
159
+ <th className="pf-v6-c-table__th" scope="col">
160
+ Last activity
161
+ </th>
162
+ <th className="pf-v6-c-table__th" scope="col">
163
+ Actions
164
+ </th>
165
+ </tr>
166
+ </thead>
167
+ <tbody className="pf-v6-c-table__tbody">
168
+ {filteredThreads.length === 0 ? (
169
+ <tr className="pf-v6-c-table__tr">
170
+ <td className="pf-v6-c-table__td" colSpan={6}>
171
+ No threads found.
172
+ </td>
173
+ </tr>
174
+ ) : (
175
+ filteredThreads.map((thread) => {
176
+ const isOpen = !!expanded[thread.id];
177
+ const last = thread.comments?.[thread.comments.length - 1]?.createdAt ?? '';
178
+ return (
179
+ <React.Fragment key={thread.id}>
180
+ <tr className="pf-v6-c-table__tr">
181
+ <td className="pf-v6-c-table__td">
182
+ <Button variant="plain" onClick={() => toggleExpanded(thread.id)} aria-label="Toggle thread">
183
+ {isOpen ? <AngleDownIcon /> : <AngleRightIcon />}
184
+ </Button>
185
+ </td>
186
+ <td className="pf-v6-c-table__td">
187
+ <Button
188
+ variant="link"
189
+ isInline
190
+ onClick={() => {
191
+ if (!isUsingDummyData) goToPin(thread.id, thread.route);
192
+ }}
193
+ isDisabled={isUsingDummyData}
194
+ >
195
+ {thread.route}
196
+ </Button>
197
+ </td>
198
+ <td className="pf-v6-c-table__td">
199
+ ({thread.xPercent.toFixed(1)}%, {thread.yPercent.toFixed(1)}%)
200
+ </td>
201
+ <td className="pf-v6-c-table__td">{thread.comments.length}</td>
202
+ <td className="pf-v6-c-table__td">{formatDate(last)}</td>
203
+ <td className="pf-v6-c-table__td">
204
+ <Button
205
+ variant="secondary"
206
+ onClick={() => {
207
+ if (!isUsingDummyData) goToPin(thread.id, thread.route);
208
+ }}
209
+ isDisabled={isUsingDummyData}
210
+ >
211
+ Go to pin
212
+ </Button>
213
+ </td>
214
+ </tr>
215
+
216
+ {isOpen && (
217
+ <tr className="pf-v6-c-table__tr">
218
+ <td className="pf-v6-c-table__td" colSpan={6}>
219
+ <div
220
+ style={{
221
+ display: 'grid',
222
+ gap: 'var(--pf-t--global--spacer--sm)',
223
+ padding: 'var(--pf-t--global--spacer--md)',
224
+ }}
225
+ >
226
+ {thread.comments.length === 0 ? (
227
+ <p style={{ color: 'var(--pf-t--global--text--color--subtle)' }}>No comments yet.</p>
228
+ ) : (
229
+ thread.comments.map((c, idx) => (
230
+ <Card key={c.id} isCompact>
231
+ <CardBody>
232
+ <div style={{ fontWeight: 'var(--pf-t--global--font--weight--bold)' }}>
233
+ Comment #{idx + 1}
234
+ </div>
235
+ <div
236
+ style={{
237
+ fontSize: 'var(--pf-t--global--font--size--sm)',
238
+ color: 'var(--pf-t--global--text--color--subtle)',
239
+ marginTop: 'var(--pf-t--global--spacer--xs)',
240
+ }}
241
+ >
242
+ @— &nbsp; {formatDate(c.createdAt)}
243
+ </div>
244
+ <div
245
+ style={{
246
+ marginTop: 'var(--pf-t--global--spacer--sm)',
247
+ whiteSpace: 'pre-wrap',
248
+ }}
249
+ >
250
+ {c.text}
251
+ </div>
252
+ </CardBody>
253
+ </Card>
254
+ ))
255
+ )}
256
+ </div>
257
+ </td>
258
+ </tr>
259
+ )}
260
+ </React.Fragment>
261
+ );
262
+ })
263
+ )}
264
+ </tbody>
265
+ </table>
266
+ </div>
267
+ </CardBody>
268
+ </Card>
269
+ </PageSection>
270
+ );
271
+ };
272
+
273
+ export { Comments };
@@ -0,0 +1,10 @@
1
+ import * as React from 'react';
2
+ import { PageSection, Title } from '@patternfly/react-core';
3
+
4
+ const Dashboard: React.FunctionComponent = () => (
5
+ <PageSection hasBodyWrapper={false}>
6
+ <Title headingLevel="h1" size="lg">Dashboard Page Title!</Title>
7
+ </PageSection>
8
+ )
9
+
10
+ export { Dashboard };
@@ -0,0 +1,35 @@
1
+ import * as React from 'react';
2
+ import { ExclamationTriangleIcon } from '@patternfly/react-icons';
3
+ import {
4
+ Button,
5
+ EmptyState,
6
+ EmptyStateBody,
7
+ EmptyStateFooter,
8
+ PageSection,
9
+ } from '@patternfly/react-core';
10
+ import { useNavigate } from 'react-router-dom';
11
+
12
+ const NotFound: React.FunctionComponent = () => {
13
+ function GoHomeBtn() {
14
+ const navigate = useNavigate();
15
+ function handleClick() {
16
+ navigate('/');
17
+ }
18
+ return (
19
+ <Button onClick={handleClick}>Take me home</Button>
20
+ );
21
+ }
22
+
23
+ return (
24
+ <PageSection hasBodyWrapper={false}>
25
+ <EmptyState titleText="404 Page not found" variant="full" icon={ExclamationTriangleIcon} >
26
+ <EmptyStateBody>
27
+ We didn&apos;t find a page that matches the address you navigated to.
28
+ </EmptyStateBody><EmptyStateFooter>
29
+ <GoHomeBtn />
30
+ </EmptyStateFooter></EmptyState>
31
+ </PageSection>
32
+ )
33
+ };
34
+
35
+ export { NotFound };
@@ -0,0 +1,16 @@
1
+ import * as React from 'react';
2
+ import { PageSection, Title } from '@patternfly/react-core';
3
+ import { useDocumentTitle } from '@app/utils/useDocumentTitle';
4
+
5
+ const GeneralSettings: React.FunctionComponent = () => {
6
+ useDocumentTitle("General Settings");
7
+ return (
8
+ <PageSection hasBodyWrapper={false}>
9
+ <Title headingLevel="h1" size="lg">
10
+ General Settings Page Title
11
+ </Title>
12
+ </PageSection>
13
+ );
14
+ }
15
+
16
+ export { GeneralSettings };
@@ -0,0 +1,18 @@
1
+ import * as React from 'react';
2
+ import { PageSection, Title } from '@patternfly/react-core';
3
+ import { useDocumentTitle } from '@app/utils/useDocumentTitle';
4
+
5
+ const ProfileSettings: React.FunctionComponent = () => {
6
+ useDocumentTitle("Profile Settings");
7
+
8
+ return (
9
+ <PageSection hasBodyWrapper={false}>
10
+ <Title headingLevel="h1" size="lg">
11
+ Profile Settings Page Title
12
+ </Title>
13
+ </PageSection>
14
+ );
15
+
16
+ }
17
+
18
+ export { ProfileSettings };
@@ -0,0 +1,50 @@
1
+ import * as React from 'react';
2
+ import { CubesIcon } from '@patternfly/react-icons';
3
+ import {
4
+ Button,
5
+ Content,
6
+ ContentVariants,
7
+ EmptyState,
8
+ EmptyStateActions,
9
+ EmptyStateBody,
10
+ EmptyStateFooter,
11
+ EmptyStateVariant,
12
+ PageSection,
13
+ } from '@patternfly/react-core';
14
+
15
+ export interface ISupportProps {
16
+ sampleProp?: string;
17
+ }
18
+
19
+ // eslint-disable-next-line prefer-const
20
+ let Support: React.FunctionComponent<ISupportProps> = () => (
21
+ <PageSection hasBodyWrapper={false}>
22
+ <EmptyState variant={EmptyStateVariant.full} titleText="Empty State (Stub Support Module)" icon={CubesIcon}>
23
+ <EmptyStateBody>
24
+ <Content>
25
+ <Content component="p">
26
+ This represents an the empty state pattern in Patternfly. Hopefully it&apos;s simple enough to use but
27
+ flexible enough to meet a variety of needs.
28
+ </Content>
29
+ <Content component={ContentVariants.small}>
30
+ This text has overridden a css component variable to demonstrate how to apply customizations using
31
+ PatternFly&apos;s CSS tokens.
32
+ </Content>
33
+ </Content>
34
+ </EmptyStateBody>
35
+ <EmptyStateFooter>
36
+ <Button variant="primary">Primary Action</Button>
37
+ <EmptyStateActions>
38
+ <Button variant="link">Multiple</Button>
39
+ <Button variant="link">Action Buttons</Button>
40
+ <Button variant="link">Can</Button>
41
+ <Button variant="link">Go here</Button>
42
+ <Button variant="link">In the secondary</Button>
43
+ <Button variant="link">Action area</Button>
44
+ </EmptyStateActions>
45
+ </EmptyStateFooter>
46
+ </EmptyState>
47
+ </PageSection>
48
+ );
49
+
50
+ export { Support };