create-hhmi-example 1.0.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 (48) hide show
  1. package/README.md +78 -0
  2. package/copy-template.js +76 -0
  3. package/index.js +254 -0
  4. package/package.json +17 -0
  5. package/template/hhmiExample.Server/Program.cs +167 -0
  6. package/template/hhmiExample.Server/Properties/launchSettings.json +44 -0
  7. package/template/hhmiExample.Server/appsettings.Development.json +8 -0
  8. package/template/hhmiExample.Server/appsettings.json +9 -0
  9. package/template/hhmiExample.Server/hhmiExample.Server.csproj +50 -0
  10. package/template/hhmiExample.Server/hhmiExample.Server.http +6 -0
  11. package/template/hhmiExample.sln +33 -0
  12. package/template/hhmiexample.client/eslint.config.js +23 -0
  13. package/template/hhmiexample.client/hhmiexample.client.esproj +12 -0
  14. package/template/hhmiexample.client/index.html +13 -0
  15. package/template/hhmiexample.client/package-lock.json +6490 -0
  16. package/template/hhmiexample.client/package.json +42 -0
  17. package/template/hhmiexample.client/prompts/README.md +12 -0
  18. package/template/hhmiexample.client/prompts/REQUIREMENTS.md +113 -0
  19. package/template/hhmiexample.client/public/favicon.ico +0 -0
  20. package/template/hhmiexample.client/public/vite.svg +1 -0
  21. package/template/hhmiexample.client/src/App.css +11 -0
  22. package/template/hhmiexample.client/src/App.tsx +147 -0
  23. package/template/hhmiexample.client/src/assets/logo-black.png +0 -0
  24. package/template/hhmiexample.client/src/assets/logo-white.png +0 -0
  25. package/template/hhmiexample.client/src/assets/react.svg +1 -0
  26. package/template/hhmiexample.client/src/components/AppFrame/AppFrame.tsx +796 -0
  27. package/template/hhmiexample.client/src/components/AppFrame/Theme.tsx +98 -0
  28. package/template/hhmiexample.client/src/components/AppFrame/UserSettingPage.tsx +91 -0
  29. package/template/hhmiexample.client/src/components/AppFrame/UserSettings.tsx +146 -0
  30. package/template/hhmiexample.client/src/components/AppFrame/modules/ExampleConfig.tsx +86 -0
  31. package/template/hhmiexample.client/src/components/AppFrame/modules/index.ts +8 -0
  32. package/template/hhmiexample.client/src/components/AppFrame/types.ts +48 -0
  33. package/template/hhmiexample.client/src/components/Global/HHMIControls.tsx +567 -0
  34. package/template/hhmiexample.client/src/components/Global/Quill.tsx +60 -0
  35. package/template/hhmiexample.client/src/index.css +11 -0
  36. package/template/hhmiexample.client/src/main.tsx +17 -0
  37. package/template/hhmiexample.client/src/pages/Example/ExampleConfigurationPage.tsx +24 -0
  38. package/template/hhmiexample.client/src/pages/Example/ExampleHomePage.tsx +23 -0
  39. package/template/hhmiexample.client/src/pages/LandingPage.tsx +36 -0
  40. package/template/hhmiexample.client/src/pages/NotAuthorizedPage.tsx +18 -0
  41. package/template/hhmiexample.client/src/services/AppService.ts +297 -0
  42. package/template/hhmiexample.client/src/types/IExampleUser.ts +19 -0
  43. package/template/hhmiexample.client/src/types/IMessageLocation.ts +8 -0
  44. package/template/hhmiexample.client/src/vite-env.d.ts +4 -0
  45. package/template/hhmiexample.client/tsconfig.app.json +27 -0
  46. package/template/hhmiexample.client/tsconfig.json +11 -0
  47. package/template/hhmiexample.client/tsconfig.node.json +25 -0
  48. package/template/hhmiexample.client/vite.config.ts +61 -0
@@ -0,0 +1,796 @@
1
+ import { makeStyles, tokens, typographyStyles, Button, Text, Menu, MenuTrigger, MenuPopover, MenuList, MenuItem, Divider, mergeClasses, Tooltip } from "@fluentui/react-components";
2
+ import { ChevronDown16Regular, ChevronUp16Regular, Shield20Regular } from "@fluentui/react-icons";
3
+ import { useState, useEffect, useMemo, useCallback } from "react";
4
+ import { Link, Outlet, useNavigate, useLocation } from "react-router";
5
+ import UserSettings from "./UserSettings";
6
+ import type { IExampleUser } from "../../types/IExampleUser";
7
+ import logoLight from "../../assets/logo-black.png";
8
+ import logoDark from "../../assets/logo-white.png";
9
+ import { CollabEnvironment } from "../../App";
10
+ import { moduleRegistry } from "./modules";
11
+ import type { INavDividerItem, INavItem, INavLinkItem } from "./types";
12
+
13
+ export default function AppFrame(props: { setCurrentUser: React.Dispatch<React.SetStateAction<IExampleUser>>; currentUser: IExampleUser; environment: string; }) {
14
+ const isProd: boolean = props.environment === CollabEnvironment.Prod;
15
+
16
+ const useStyles = makeStyles({
17
+ appContainer: {
18
+ display: 'flex',
19
+ flexDirection: 'column',
20
+ minHeight: '100vh',
21
+ backgroundColor: tokens.colorNeutralBackground1
22
+ },
23
+ appHeader: {
24
+ display: 'flex',
25
+ justifyContent: 'space-between',
26
+ alignItems: 'center',
27
+ padding: '12px 16px',
28
+ backgroundColor: isProd ? tokens.colorNeutralBackground1 : tokens.colorBrandBackground,
29
+ borderBottom: `1px solid ${tokens.colorNeutralStroke2}`,
30
+ height: '64px',
31
+ position: 'sticky',
32
+ top: 0,
33
+ zIndex: 100,
34
+ boxShadow: tokens.shadow2
35
+ },
36
+ headerLeft: {
37
+ display: 'flex',
38
+ alignItems: 'center',
39
+ gap: '1em'
40
+ },
41
+ brandSection: {
42
+ display: 'flex',
43
+ alignItems: 'center',
44
+ gap: '12px'
45
+ },
46
+ companyName: {
47
+ color: tokens.colorBrandForeground1,
48
+ fontWeight: tokens.fontWeightSemibold,
49
+ fontSize: tokens.fontSizeBase500,
50
+ textDecoration: 'none',
51
+ transition: 'color 0.2s ease',
52
+ ':hover': {
53
+ color: tokens.colorBrandForeground2Hover,
54
+ textDecoration: 'none'
55
+ },
56
+ ':focus': {
57
+ outline: `2px solid ${tokens.colorBrandStroke1}`,
58
+ outlineOffset: '2px',
59
+ borderRadius: tokens.borderRadiusSmall
60
+ }
61
+ },
62
+ logoImage: {
63
+ height: '42px',
64
+ marginRight: '8px',
65
+ verticalAlign: 'middle'
66
+ },
67
+ separator: {
68
+ width: '2px',
69
+ height: '30px',
70
+ backgroundColor: isProd ? tokens.colorNeutralStroke2 : tokens.colorBrandBackgroundStatic,
71
+ },
72
+ separatorMobile: {
73
+ width: '2px',
74
+ height: '30px',
75
+ backgroundColor: isProd ? tokens.colorNeutralStroke2 : tokens.colorBrandBackgroundStatic,
76
+ margin: '0 15px'
77
+ },
78
+ pageTitle: {
79
+ color: isProd ? tokens.colorNeutralForeground2 : tokens.colorNeutralForegroundOnBrand,
80
+ fontSize: tokens.fontSizeBase500,
81
+ fontWeight: tokens.fontWeightMedium,
82
+ '@media (max-width: 768px)': {
83
+ display: 'none'
84
+ }
85
+ },
86
+ pageTitleMobile: {
87
+ color: tokens.colorNeutralForeground2,
88
+ fontSize: tokens.fontSizeBase500,
89
+ fontWeight: tokens.fontWeightMedium,
90
+ lineHeight: 'normal'
91
+ },
92
+ modulesNav: {
93
+ display: 'flex',
94
+ alignItems: 'center',
95
+ gap: '0',
96
+ '@media (max-width: 768px)': {
97
+ display: 'none'
98
+ }
99
+ },
100
+ moduleButtonDesktop: {
101
+ backgroundColor: 'transparent',
102
+ border: 'none',
103
+ padding: '8px 16px',
104
+ cursor: 'pointer',
105
+ color: isProd ? tokens.colorNeutralForeground2 : tokens.colorNeutralForegroundOnBrand,
106
+ fontSize: tokens.fontSizeBase500,
107
+ fontWeight: tokens.fontWeightMedium,
108
+ textDecoration: 'none',
109
+ transition: 'background-color 0.2s ease',
110
+ borderRadius: tokens.borderRadiusSmall,
111
+ ':hover': {
112
+ backgroundColor: tokens.colorTransparentBackground
113
+ }
114
+ },
115
+ moduleSeparator: {
116
+ width: '2px',
117
+ height: '30px',
118
+ backgroundColor: isProd ? tokens.colorNeutralStroke2 : tokens.colorBrandBackgroundStatic,
119
+ margin: '0 8px',
120
+ },
121
+ mobileModulesMenu: {
122
+ display: 'none',
123
+ '@media (max-width: 768px)': {
124
+ display: 'flex'
125
+ }
126
+ },
127
+ mobileModulesButton: {
128
+ minHeight: '32px',
129
+ padding: '5px 0em',
130
+ fontSize: tokens.fontSizeBase500,
131
+ fontWeight: tokens.fontWeightMedium,
132
+ '@media (max-width: 768px)': {
133
+ display: 'flex'
134
+ }
135
+ },
136
+ moduleButton: {
137
+ minWidth: 'auto',
138
+ fontWeight: tokens.fontWeightMedium,
139
+ color: tokens.colorBrandBackgroundInverted,
140
+ backgroundColor: isProd ? tokens.colorBrandBackground : tokens.colorBrandBackgroundSelected,
141
+ display: 'flex',
142
+ alignItems: 'center',
143
+ gap: '8px'
144
+ },
145
+ moduleButtonContent: {
146
+ display: 'flex',
147
+ alignItems: 'center',
148
+ gap: '8px'
149
+ },
150
+ mobileMenuToggle: {
151
+ display: 'none'
152
+ },
153
+ appContent: {
154
+ display: 'flex',
155
+ flex: 1,
156
+ overflow: 'hidden',
157
+ position: 'relative'
158
+ },
159
+ sidebar: {
160
+ width: '300px',
161
+ backgroundColor: tokens.colorNeutralBackground2,
162
+ borderRight: `1px solid ${tokens.colorNeutralStroke2}`,
163
+ padding: '16px',
164
+ overflowY: 'auto',
165
+ overflowX: 'hidden',
166
+ position: 'relative',
167
+ zIndex: 10,
168
+ '@media (max-width: 768px)': {
169
+ display: 'none'
170
+ }
171
+ },
172
+ sidebarBorder: {
173
+ position: 'absolute',
174
+ right: 0,
175
+ top: 0,
176
+ bottom: 0,
177
+ width: '4px',
178
+ cursor: 'default',
179
+ zIndex: 1001,
180
+ transition: 'width 0.2s ease, background-color 0.2s ease',
181
+ ':hover': {
182
+ width: '6px',
183
+ cursor: 'pointer',
184
+ backgroundColor: tokens.colorNeutralStroke1,
185
+ }
186
+ },
187
+ sidebarCollapsed: {
188
+ width: '56px',
189
+ padding: '16px 8px',
190
+ overflowX: 'hidden',
191
+ overflowY: 'auto',
192
+ },
193
+ sidebarNav: {
194
+ '& ul': {
195
+ listStyle: 'none',
196
+ margin: 0,
197
+ padding: 0,
198
+ width: '100%'
199
+ },
200
+ '& li': {
201
+ marginBottom: '4px',
202
+ width: '100%'
203
+ }
204
+ },
205
+ navButton: {
206
+ width: '100%',
207
+ justifyContent: 'flex-start',
208
+ minWidth: 'auto'
209
+ },
210
+ navButtonCollapsed: {
211
+ justifyContent: 'center',
212
+ padding: '8px',
213
+ width: '100%',
214
+ minWidth: '100%',
215
+ },
216
+ navButtonCollapsedSelected: {
217
+ width: '100%',
218
+ minWidth: '100%',
219
+ },
220
+ adminSection: {
221
+ marginBottom: '16px',
222
+ padding: '12px',
223
+ border: `1px solid ${tokens.colorNeutralStroke2}`,
224
+ borderRadius: tokens.borderRadiusMedium,
225
+ width: '100%',
226
+ },
227
+ adminSectionCollapsed: {
228
+ padding: '8px 0',
229
+ },
230
+ adminSectionHeader: {
231
+ display: 'flex',
232
+ alignItems: 'center',
233
+ justifyContent: 'space-between',
234
+ gap: '8px',
235
+ fontSize: tokens.fontSizeBase300,
236
+ fontWeight: tokens.fontWeightSemibold,
237
+ color: tokens.colorBrandForeground1,
238
+ textTransform: 'uppercase',
239
+ letterSpacing: '0.5px',
240
+ cursor: 'pointer',
241
+ userSelect: 'none',
242
+ ':hover': {
243
+ color: tokens.colorBrandForeground2Hover,
244
+ }
245
+ },
246
+ adminSectionHeaderCollapsed: {
247
+ justifyContent: 'center',
248
+ gap: 0,
249
+ },
250
+ adminSectionHeaderContent: {
251
+ display: 'flex',
252
+ alignItems: 'center',
253
+ gap: '8px',
254
+ },
255
+ adminSectionHeaderContentCollapsed: {
256
+ gap: 0,
257
+ },
258
+ adminSectionHeaderText: {
259
+ '@media (max-width: 768px)': {
260
+ display: 'none'
261
+ }
262
+ },
263
+ adminNavButton: {
264
+ width: '100%',
265
+ justifyContent: 'flex-start',
266
+ minWidth: 'auto',
267
+ border: 'none'
268
+ },
269
+ adminNavButtonCollapsed: {
270
+ justifyContent: 'center',
271
+ padding: '8px',
272
+ width: '100%',
273
+ minWidth: '100%',
274
+ },
275
+ adminNavButtonCollapsedSelected: {
276
+ width: '100%',
277
+ minWidth: '100%',
278
+ },
279
+ sectionDivider: {
280
+ margin: '16px 0',
281
+ },
282
+ adminNavItem: {
283
+ marginBottom: '4px',
284
+ width: '100%',
285
+ },
286
+ adminNavItems: {
287
+ marginTop: '10px',
288
+ },
289
+ emptySideNav: {
290
+ width: '212px'
291
+ },
292
+ mainContent: {
293
+ flex: 1,
294
+ padding: '32px',
295
+ overflowY: "auto",
296
+ overflowX: 'hidden',
297
+ minHeight: 0,
298
+ height: "calc(100vh - 64px)",
299
+ backgroundColor: tokens.colorNeutralBackground1,
300
+ position: 'relative',
301
+ zIndex: 1,
302
+ '@media (max-width: 768px)': {
303
+ padding: '16px',
304
+ paddingBottom: '80px'
305
+ }
306
+ },
307
+ contentWrapper: {},
308
+ contentGrid: {
309
+ display: 'grid',
310
+ gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))',
311
+ gap: '24px',
312
+ marginTop: '32px',
313
+ '@media (max-width: 768px)': {
314
+ gridTemplateColumns: '1fr',
315
+ gap: '16px'
316
+ }
317
+ },
318
+ bottomNav: {
319
+ display: 'none',
320
+ '@media (max-width: 768px)': {
321
+ display: 'flex',
322
+ position: 'fixed',
323
+ bottom: 0,
324
+ left: 0,
325
+ right: 0,
326
+ backgroundColor: tokens.colorNeutralBackground1,
327
+ borderTop: `1px solid ${tokens.colorNeutralStroke2}`,
328
+ padding: '8px 12px',
329
+ justifyContent: 'space-between',
330
+ alignItems: 'center',
331
+ zIndex: 100,
332
+ boxShadow: `0 -2px 8px ${tokens.colorNeutralShadowAmbient}`,
333
+ height: '72px',
334
+ minHeight: '72px',
335
+ '@media (max-width: 480px)': {
336
+ padding: '6px 8px',
337
+ height: '68px',
338
+ minHeight: '68px'
339
+ }
340
+ }
341
+ },
342
+ bottomNavButton: {
343
+ display: 'flex',
344
+ flexDirection: 'column',
345
+ alignItems: 'center',
346
+ justifyContent: 'center',
347
+ gap: '4px',
348
+ flex: '1',
349
+ minHeight: '56px',
350
+ padding: '8px 4px',
351
+ backgroundColor: 'transparent',
352
+ border: 'none',
353
+ borderRadius: tokens.borderRadiusMedium,
354
+ cursor: 'pointer',
355
+ fontSize: '10px',
356
+ fontWeight: tokens.fontWeightMedium,
357
+ color: tokens.colorNeutralForeground2,
358
+ transition: 'all 0.2s ease',
359
+ textAlign: 'center',
360
+ '@media (max-width: 480px)': {
361
+ minHeight: '52px',
362
+ padding: '6px 2px',
363
+ },
364
+ ':hover': {
365
+ backgroundColor: tokens.colorNeutralBackground1Hover,
366
+ color: tokens.colorBrandForeground1
367
+ },
368
+ ':focus': {
369
+ outline: `2px solid ${tokens.colorBrandStroke1}`,
370
+ outlineOffset: '1px'
371
+ }
372
+ },
373
+ bottomNavButtonActive: {
374
+ color: tokens.colorNeutralForegroundOnBrand,
375
+ backgroundColor: tokens.colorBrandBackground
376
+ },
377
+ bottomNavIcon: {
378
+ fontSize: '20px',
379
+ lineHeight: 1
380
+ },
381
+ bottomNavLabel: {
382
+ fontSize: '10px',
383
+ lineHeight: 1.2,
384
+ textAlign: 'center',
385
+ maxWidth: '100px',
386
+ overflow: 'hidden',
387
+ textOverflow: 'ellipsis',
388
+ whiteSpace: 'nowrap',
389
+ fontWeight: tokens.fontWeightMedium,
390
+ '@media (max-width: 480px)': {
391
+ fontSize: '9px'
392
+ }
393
+ },
394
+ mainContentNoScroll: {
395
+ overflowY: 'hidden'
396
+ },
397
+ navDivider: {
398
+ margin: '8px 0',
399
+ borderTop: `1px solid ${tokens.colorNeutralStroke2}`,
400
+ opacity: 0.4
401
+ },
402
+ navDividerCollapsed: {
403
+ margin: '8px auto',
404
+ width: '60%',
405
+ borderTop: `1px solid ${tokens.colorNeutralStroke2}`,
406
+ opacity: 0.4
407
+ },
408
+ title: typographyStyles.title2,
409
+ paragraph: {
410
+ ...typographyStyles.body1,
411
+ letterSpacing: '0.0675em',
412
+ fontStyle: 'italic',
413
+ }
414
+ });
415
+ const styles = useStyles();
416
+
417
+ const navigate = useNavigate();
418
+ const location = useLocation();
419
+
420
+ const [activeModule, setActiveModule] = useState<string>("");
421
+ const [activeNav, setActiveNav] = useState<string>('');
422
+ const [currentModuleLabel, setCurrentModuleLabel] = useState<string>("");
423
+ const [isAdminExpanded, setIsAdminExpanded] = useState<boolean>(false);
424
+ const [hasUserToggledAdmin, setHasUserToggledAdmin] = useState<boolean>(false);
425
+
426
+ const [isSidebarCollapsed, setIsSidebarCollapsed] = useState<boolean>(() => {
427
+ const saved = localStorage.getItem('sidebarCollapsed');
428
+ return saved ? saved === 'true' : false;
429
+ });
430
+
431
+ useEffect(() => {
432
+ localStorage.setItem('sidebarCollapsed', isSidebarCollapsed.toString());
433
+ }, [isSidebarCollapsed]);
434
+
435
+ const modules: INavLinkItem[] = useMemo(() => {
436
+ return Object.values(moduleRegistry)
437
+ .map(config => ({
438
+ id: config.id,
439
+ label: config.label,
440
+ icon: config.icon,
441
+ href: config.href
442
+ }));
443
+ }, []);
444
+
445
+ const handleModuleSelect = (module: INavLinkItem, isNavigate: boolean) => {
446
+ setActiveModule(module.id);
447
+ setCurrentModuleLabel(modules.find(m => m.id === module.id)?.label ?? "");
448
+ if (isNavigate) {
449
+ navigate(module.href, { replace: true });
450
+ }
451
+ }
452
+
453
+ const currentModuleConfig = useMemo(() => {
454
+ if (!activeModule) return null;
455
+ return Object.values(moduleRegistry).find(c => c.id === activeModule) ?? null;
456
+ }, [activeModule]);
457
+
458
+ const userPermissions = useMemo(() => {
459
+ if (!currentModuleConfig) return {};
460
+ return currentModuleConfig.resolvePermissions(props.currentUser);
461
+ }, [currentModuleConfig, props.currentUser]);
462
+
463
+ const canViewAdmin = useMemo(() => {
464
+ if (!currentModuleConfig) { return false; }
465
+ return currentModuleConfig.canViewAdmin(userPermissions);
466
+ }, [currentModuleConfig, userPermissions]);
467
+
468
+ const regularNavItems = useMemo(() => {
469
+ if (!currentModuleConfig) return [];
470
+ return currentModuleConfig.getRegularNavItems(activeModule, userPermissions);
471
+ }, [currentModuleConfig, activeModule, userPermissions]);
472
+
473
+ const adminNavItems = useMemo(() => {
474
+ if (!currentModuleConfig) return [];
475
+ return currentModuleConfig.getAdminNavItems(activeModule, userPermissions);
476
+ }, [currentModuleConfig, activeModule, userPermissions]);
477
+
478
+ const mobileAdminNavItems = useMemo(() => {
479
+ if (!currentModuleConfig) return [];
480
+ return currentModuleConfig.getMobileAdminNavItem(activeModule, canViewAdmin);
481
+ }, [currentModuleConfig, activeModule, canViewAdmin]);
482
+
483
+ const isNavDividerItem = useCallback((item: INavItem): item is INavDividerItem => {
484
+ return (item as INavDividerItem).isDivider === true;
485
+ }, []);
486
+
487
+ const isNavLinkItem = useCallback((item: INavItem): item is INavLinkItem => {
488
+ return !isNavDividerItem(item);
489
+ }, [isNavDividerItem]);
490
+
491
+ useEffect(() => {
492
+ const currentPath = location.pathname;
493
+ const pathSegments = currentPath.split('/').filter(Boolean);
494
+
495
+ if (currentPath === '/user-settings') {
496
+ return;
497
+ }
498
+
499
+ if (pathSegments.length >= 1) {
500
+ const pathModule = pathSegments[0];
501
+ const matchedModule = modules.find(m => m.id.toLowerCase() === pathModule.toLowerCase());
502
+ if (matchedModule) {
503
+ const properCaseModule = matchedModule.id;
504
+ const moduleConfig = Object.values(moduleRegistry).find(c => c.id === properCaseModule) ?? null;
505
+ if (moduleConfig?.navItems) {
506
+ const resolvedPermissions = moduleConfig.resolvePermissions(props.currentUser);
507
+ setActiveModule(properCaseModule);
508
+ setCurrentModuleLabel(matchedModule.label);
509
+
510
+ const regularNavItemsForModule = moduleConfig.getRegularNavItems(properCaseModule, resolvedPermissions);
511
+ const adminNavItemsForModule = moduleConfig.getAdminNavItems(properCaseModule, resolvedPermissions);
512
+ const normalizedPath = currentPath.toLowerCase().replace(/\/$/, '');
513
+
514
+ if (pathSegments.length === 1) {
515
+ const firstRegularNav = regularNavItemsForModule.find(isNavLinkItem);
516
+ if (firstRegularNav) {
517
+ setActiveNav(firstRegularNav.id);
518
+ }
519
+ } else if (pathSegments.length >= 2) {
520
+ if (pathSegments[1] === 'admin') {
521
+ if (pathSegments.length === 2) {
522
+ if (moduleConfig.navItems.Admin) {
523
+ setActiveNav(moduleConfig.navItems.Admin);
524
+ }
525
+ } else if (pathSegments.length >= 3) {
526
+ const matchingNavItem = adminNavItemsForModule.filter(isNavLinkItem).find(item => {
527
+ const itemPath = item.href.toLowerCase().replace(/\/$/, '');
528
+ return normalizedPath === itemPath || normalizedPath.startsWith(itemPath + '/');
529
+ });
530
+ if (matchingNavItem) {
531
+ setActiveNav(matchingNavItem.id);
532
+ if (!hasUserToggledAdmin) setIsAdminExpanded(true);
533
+ } else {
534
+ const adminRoute = pathSegments[2];
535
+ const adminNavId = `Admin${adminRoute.charAt(0).toUpperCase() + adminRoute.slice(1)}`;
536
+ if (moduleConfig.navItems[adminNavId]) {
537
+ setActiveNav(moduleConfig.navItems[adminNavId]);
538
+ if (!hasUserToggledAdmin) setIsAdminExpanded(true);
539
+ } else {
540
+ setActiveNav(moduleConfig.navItems.Admin || '');
541
+ }
542
+ }
543
+ }
544
+ } else {
545
+ const matchingRegular = regularNavItemsForModule.filter(isNavLinkItem).find(item => {
546
+ const itemPath = item.href.toLowerCase().replace(/\/$/, '');
547
+ return normalizedPath === itemPath;
548
+ });
549
+ if (matchingRegular) {
550
+ setActiveNav(matchingRegular.id);
551
+ } else {
552
+ const matchingAdmin = adminNavItemsForModule.filter(isNavLinkItem).find(item => {
553
+ const itemPath = item.href.toLowerCase().replace(/\/$/, '');
554
+ return normalizedPath === itemPath || normalizedPath.startsWith(itemPath + '/');
555
+ });
556
+ if (matchingAdmin) {
557
+ setActiveNav(matchingAdmin.id);
558
+ if (!hasUserToggledAdmin) setIsAdminExpanded(true);
559
+ } else {
560
+ const firstRegularNav = regularNavItemsForModule.find(isNavLinkItem);
561
+ if (firstRegularNav) setActiveNav(firstRegularNav.id);
562
+ }
563
+ }
564
+ }
565
+ }
566
+ }
567
+ } else {
568
+ setActiveModule('');
569
+ setCurrentModuleLabel('');
570
+ setActiveNav('');
571
+ }
572
+ } else {
573
+ setActiveModule('');
574
+ setCurrentModuleLabel('');
575
+ setActiveNav('');
576
+ }
577
+ }, [location.pathname, modules, hasUserToggledAdmin, isNavLinkItem, props.currentUser]);
578
+
579
+ return <div className={styles.appContainer}>
580
+ <header className={styles.appHeader}>
581
+ <div className={styles.headerLeft}>
582
+ <div className={styles.brandSection}>
583
+ <Link to="/" className={styles.companyName}>
584
+ <img
585
+ src={(props.currentUser.IsDarkTheme || !isProd) ? logoDark : logoLight}
586
+ alt="Company Logo"
587
+ className={styles.logoImage}
588
+ />
589
+ </Link>
590
+ <div className={styles.separator} />
591
+ <Text className={styles.pageTitle}>Example App {!isProd ? `- ${props.environment}` : ""}</Text>
592
+ </div>
593
+
594
+ <nav className={styles.modulesNav}>
595
+ <div className={styles.separator} />
596
+ {modules.map((module) => (
597
+ <Button
598
+ key={module.id}
599
+ className={styles.moduleButtonDesktop}
600
+ onClick={() => { handleModuleSelect(module, true); }}
601
+ aria-label={`Navigate to ${module.label}`}
602
+ >
603
+ {module.label}
604
+ </Button>
605
+ ))}
606
+ </nav>
607
+
608
+ <div className={styles.mobileModulesMenu}>
609
+ {currentModuleLabel && (
610
+ <>
611
+ <Text className={styles.pageTitleMobile}>{currentModuleLabel}</Text>
612
+ <div className={styles.separatorMobile} />
613
+ </>
614
+ )}
615
+ <Menu>
616
+ <MenuTrigger disableButtonEnhancement>
617
+ <Button
618
+ appearance="subtle"
619
+ iconPosition="after"
620
+ icon={<ChevronDown16Regular />}
621
+ className={styles.mobileModulesButton}
622
+ aria-label="Select app"
623
+ >
624
+ {"Select App"}
625
+ </Button>
626
+ </MenuTrigger>
627
+ <MenuPopover>
628
+ <MenuList>
629
+ {modules.map((module) => (
630
+ <MenuItem
631
+ key={module.id}
632
+ icon={<module.icon />}
633
+ onClick={() => { handleModuleSelect(module, true); }}
634
+ >
635
+ {module.label}
636
+ </MenuItem>
637
+ ))}
638
+ </MenuList>
639
+ </MenuPopover>
640
+ </Menu>
641
+ </div>
642
+ </div>
643
+
644
+ <UserSettings
645
+ currentUser={props.currentUser}
646
+ setCurrentUser={props.setCurrentUser as React.Dispatch<React.SetStateAction<IExampleUser | undefined>>}
647
+ isProd={isProd}
648
+ />
649
+ </header>
650
+
651
+ <div className={styles.appContent}>
652
+ <aside className={mergeClasses(styles.sidebar, isSidebarCollapsed && styles.sidebarCollapsed)}>
653
+ {activeModule && (
654
+ <div
655
+ className={styles.sidebarBorder}
656
+ onClick={() => setIsSidebarCollapsed(!isSidebarCollapsed)}
657
+ aria-label={isSidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"}
658
+ role="button"
659
+ tabIndex={0}
660
+ onKeyDown={(e) => {
661
+ if (e.key === 'Enter' || e.key === ' ') {
662
+ e.preventDefault();
663
+ setIsSidebarCollapsed(!isSidebarCollapsed);
664
+ }
665
+ }}
666
+ />
667
+ )}
668
+ <nav className={styles.sidebarNav}>
669
+ {activeModule ? (
670
+ <ul>
671
+ {canViewAdmin && (
672
+ <>
673
+ <li>
674
+ <div className={mergeClasses(styles.adminSection, isSidebarCollapsed && styles.adminSectionCollapsed)}>
675
+ {isSidebarCollapsed ? (
676
+ <Tooltip relationship="label" content="Configuration">
677
+ <div
678
+ className={mergeClasses(styles.adminSectionHeader, styles.adminSectionHeaderCollapsed)}
679
+ onClick={() => {
680
+ setIsAdminExpanded(!isAdminExpanded);
681
+ setHasUserToggledAdmin(true);
682
+ }}
683
+ >
684
+ <div className={mergeClasses(styles.adminSectionHeaderContent, styles.adminSectionHeaderContentCollapsed)}>
685
+ <Shield20Regular />
686
+ </div>
687
+ </div>
688
+ </Tooltip>
689
+ ) : (
690
+ <div
691
+ className={styles.adminSectionHeader}
692
+ onClick={() => {
693
+ setIsAdminExpanded(!isAdminExpanded);
694
+ setHasUserToggledAdmin(true);
695
+ }}
696
+ >
697
+ <div className={styles.adminSectionHeaderContent}>
698
+ <Shield20Regular />
699
+ <span className={styles.adminSectionHeaderText}>Configuration</span>
700
+ </div>
701
+ {isAdminExpanded ? <ChevronUp16Regular /> : <ChevronDown16Regular />}
702
+ </div>
703
+ )}
704
+ {isAdminExpanded && (
705
+ <div className={styles.adminNavItems}>
706
+ {adminNavItems.map((item) => (
707
+ <div key={item.id} className={styles.adminNavItem}>
708
+ {isNavDividerItem(item) ? (
709
+ <Divider className={mergeClasses(styles.navDivider, isSidebarCollapsed && styles.navDividerCollapsed)} />
710
+ ) : isSidebarCollapsed ? (
711
+ <Tooltip relationship="label" content={item.label}>
712
+ <Button
713
+ appearance={activeNav === item.id ? 'primary' : 'subtle'}
714
+ icon={<item.icon />}
715
+ className={mergeClasses(styles.adminNavButton, styles.adminNavButtonCollapsed, activeNav === item.id && styles.adminNavButtonCollapsedSelected)}
716
+ onClick={() => { navigate(item.href, { replace: true }); }}
717
+ />
718
+ </Tooltip>
719
+ ) : (
720
+ <Button
721
+ appearance={activeNav === item.id ? 'primary' : 'subtle'}
722
+ icon={<item.icon />}
723
+ className={styles.adminNavButton}
724
+ onClick={() => { navigate(item.href, { replace: true }); }}
725
+ >
726
+ {item.label}
727
+ </Button>
728
+ )}
729
+ </div>
730
+ ))}
731
+ </div>
732
+ )}
733
+ </div>
734
+ </li>
735
+ {!isSidebarCollapsed && <Divider className={styles.sectionDivider} />}
736
+ </>
737
+ )}
738
+
739
+ {regularNavItems.map((item: INavItem) => (
740
+ <li key={item.id}>
741
+ {isNavDividerItem(item) ? (
742
+ <Divider className={mergeClasses(styles.navDivider, isSidebarCollapsed && styles.navDividerCollapsed)} />
743
+ ) : isSidebarCollapsed ? (
744
+ <Tooltip relationship="label" content={item.label}>
745
+ <Button
746
+ appearance={activeNav === item.id ? 'primary' : 'subtle'}
747
+ icon={<item.icon />}
748
+ className={mergeClasses(styles.navButton, styles.navButtonCollapsed, activeNav === item.id && styles.navButtonCollapsedSelected)}
749
+ onClick={() => { navigate(item.href, { replace: true }); }}
750
+ />
751
+ </Tooltip>
752
+ ) : (
753
+ <Button
754
+ appearance={activeNav === item.id ? 'primary' : 'subtle'}
755
+ icon={<item.icon />}
756
+ className={styles.navButton}
757
+ onClick={() => { navigate(item.href, { replace: true }); }}
758
+ >
759
+ {item.label}
760
+ </Button>
761
+ )}
762
+ </li>
763
+ ))}
764
+ </ul>
765
+ ) : (
766
+ <ul className={styles.emptySideNav} />
767
+ )}
768
+ </nav>
769
+ </aside>
770
+
771
+ <main className={styles.mainContent}>
772
+ <div className={styles.contentWrapper}>
773
+ <Outlet />
774
+ </div>
775
+ </main>
776
+ </div>
777
+
778
+ {activeModule && (
779
+ <nav className={styles.bottomNav}>
780
+ {[...mobileAdminNavItems, ...regularNavItems].filter(isNavLinkItem).map((item) => (
781
+ <button
782
+ key={item.id}
783
+ className={mergeClasses(styles.bottomNavButton, (activeNav === item.id || (currentModuleConfig?.navItems?.Admin === item.id && location.pathname.includes('/configuration'))) ? styles.bottomNavButtonActive : '')}
784
+ onClick={() => { navigate(item.href, { replace: true }); }}
785
+ aria-label={item.label}
786
+ >
787
+ <div className={styles.bottomNavIcon}>
788
+ <item.icon />
789
+ </div>
790
+ <span className={styles.bottomNavLabel}>{item.label}</span>
791
+ </button>
792
+ ))}
793
+ </nav>
794
+ )}
795
+ </div>;
796
+ }