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,98 @@
1
+ import { createDarkTheme, createLightTheme, type BrandVariants, type Theme } from "@fluentui/react-components";
2
+
3
+ const colorNeutralForegroundDisabledLight = '#888';
4
+ const colorNeutralForegroundDisabledDark = '#999';
5
+
6
+ const brandThemeDev: BrandVariants = {
7
+ 10: "#0A0102",
8
+ 20: "#1A0408",
9
+ 30: "#2A070E",
10
+ 40: "#3A0A14",
11
+ 50: "#4A0D1A",
12
+ 60: "#5A1020",
13
+ 70: "#6A1326",
14
+ 80: "#7A162C",
15
+ 90: "#8A1932",
16
+ 100: "#9A1C38",
17
+ 110: "#AA1F3E",
18
+ 120: "#BA2244",
19
+ 130: "#CA254A",
20
+ 140: "#DA2850",
21
+ 150: "#EA2B56",
22
+ 160: "#FA2E5C"
23
+ };
24
+
25
+ export const lightThemeDev: Theme = {
26
+ ...createLightTheme(brandThemeDev),
27
+ colorNeutralForegroundDisabled: colorNeutralForegroundDisabledLight
28
+ };
29
+
30
+ export const darkThemeDev: Theme = {
31
+ ...createDarkTheme(brandThemeDev),
32
+ colorNeutralForegroundDisabled: colorNeutralForegroundDisabledDark
33
+ };
34
+
35
+ const brandThemeTest: BrandVariants = {
36
+ 10: "#000C1A",
37
+ 20: "#001A33",
38
+ 30: "#00284D",
39
+ 40: "#003666",
40
+ 50: "#004480",
41
+ 60: "#005299",
42
+ 70: "#0060B3",
43
+ 80: "#006ECC",
44
+ 90: "#007CE6",
45
+ 100: "#004EFF",
46
+ 110: "#004EFF",
47
+ 120: "#1A66FF",
48
+ 130: "#337EFF",
49
+ 140: "#4D96FF",
50
+ 150: "#66AEFF",
51
+ 160: "#80C6FF"
52
+ };
53
+
54
+ export const lightThemeTest: Theme = {
55
+ ...createLightTheme(brandThemeTest),
56
+ colorBrandBackground: brandThemeTest[110],
57
+ colorBrandBackground2: brandThemeTest[120],
58
+ colorNeutralForegroundDisabled: colorNeutralForegroundDisabledLight
59
+ };
60
+
61
+ export const darkThemeTest: Theme = {
62
+ ...createDarkTheme(brandThemeTest),
63
+ colorBrandBackground: brandThemeTest[110],
64
+ colorBrandBackground2: brandThemeTest[120],
65
+ colorNeutralForegroundDisabled: colorNeutralForegroundDisabledDark
66
+ };
67
+
68
+ const brandTheme: BrandVariants = {
69
+ 10: "#020404",
70
+ 20: "#0F1B1C",
71
+ 30: "#142D2F",
72
+ 40: "#163A3D",
73
+ 50: "#17484C",
74
+ 60: "#17565B",
75
+ 70: "#16656B",
76
+ 80: "#13747B",
77
+ 90: "#358187",
78
+ 100: "#4E8E93",
79
+ 110: "#659BA0",
80
+ 120: "#7AA9AD",
81
+ 130: "#90B6B9",
82
+ 140: "#A5C4C6",
83
+ 150: "#BAD2D4",
84
+ 160: "#CFE0E1"
85
+ };
86
+
87
+ export const lightTheme: Theme = {
88
+ ...createLightTheme(brandTheme),
89
+ colorNeutralForegroundDisabled: colorNeutralForegroundDisabledLight,
90
+ colorBrandBackgroundInvertedHover: brandTheme[100],
91
+ colorBrandBackground2: brandTheme[60]
92
+ };
93
+
94
+ export const darkTheme: Theme = {
95
+ ...createDarkTheme(brandTheme),
96
+ colorNeutralForegroundDisabled: colorNeutralForegroundDisabledDark,
97
+ colorBrandBackground2: brandTheme[60]
98
+ };
@@ -0,0 +1,91 @@
1
+ import {
2
+ makeStyles,
3
+ tokens,
4
+ Title2,
5
+ Text,
6
+ Avatar,
7
+ Switch
8
+ } from "@fluentui/react-components";
9
+ import type { IExampleUser } from "../../types/IExampleUser";
10
+
11
+ const useStyles = makeStyles({
12
+ container: {
13
+ padding: '24px',
14
+ maxWidth: '600px',
15
+ margin: '0 auto'
16
+ },
17
+ header: {
18
+ display: 'flex',
19
+ alignItems: 'center',
20
+ gap: '16px',
21
+ marginBottom: '32px'
22
+ },
23
+ backButton: {
24
+ minWidth: '40px'
25
+ },
26
+ profileSection: {
27
+ display: 'flex',
28
+ alignItems: 'center',
29
+ gap: '16px',
30
+ marginBottom: '32px',
31
+ padding: '24px',
32
+ backgroundColor: tokens.colorNeutralBackground2,
33
+ borderRadius: tokens.borderRadiusLarge
34
+ },
35
+ profileInfo: {
36
+ display: 'flex',
37
+ flexDirection: 'column',
38
+ gap: '4px'
39
+ },
40
+ settingInfo: {
41
+ display: 'flex',
42
+ flexDirection: 'column',
43
+ gap: '4px'
44
+ }
45
+ });
46
+
47
+ export default function UserSettingsPage(props: { currentUser: IExampleUser; setCurrentUser: React.Dispatch<React.SetStateAction<IExampleUser | undefined>>; }) {
48
+ const styles = useStyles();
49
+
50
+ return (
51
+ <div className={styles.container}>
52
+ <div className={styles.header}>
53
+ <Title2>User Settings</Title2>
54
+ </div>
55
+
56
+ <div className={styles.profileSection}>
57
+ <Avatar
58
+ name={props.currentUser.Name}
59
+ size={128}
60
+ color="colorful"
61
+ image={{ src: props.currentUser.PhotoURL }}
62
+ />
63
+ <div className={styles.profileInfo}>
64
+ <Text size={500} weight="semibold">{props.currentUser.Name}</Text>
65
+ <Text size={300} style={{ color: tokens.colorNeutralForeground3 }}>
66
+ {props.currentUser.UserId}
67
+ </Text>
68
+ </div>
69
+ </div>
70
+ <div className={styles.profileSection} style={{ justifyContent: 'space-between' }}>
71
+ <div className={styles.settingInfo}>
72
+ <Text weight="semibold">{props.currentUser.IsDarkTheme === true ? "Dark" : "Light"} Mode</Text>
73
+ <Text size={200} style={{ color: tokens.colorNeutralForeground3 }}>
74
+ Switch between light and dark themes
75
+ </Text>
76
+ </div>
77
+ <Switch
78
+ checked={props.currentUser.IsDarkTheme}
79
+ aria-label="Toggle dark mode"
80
+ onChange={(_ev, data) => {
81
+ const updatedUser = {
82
+ ...props.currentUser,
83
+ IsDarkTheme: data.checked
84
+ };
85
+ props.setCurrentUser(updatedUser);
86
+ }}
87
+ />
88
+ </div>
89
+ </div>
90
+ );
91
+ }
@@ -0,0 +1,146 @@
1
+ import {
2
+ Avatar,
3
+ makeStyles,
4
+ Text,
5
+ tokens,
6
+ Menu,
7
+ MenuTrigger,
8
+ MenuPopover,
9
+ MenuList,
10
+ MenuItem,
11
+ MenuDivider
12
+ } from "@fluentui/react-components";
13
+ import { DarkTheme20Regular } from "@fluentui/react-icons";
14
+ import { useNavigate } from "react-router";
15
+ import React from "react";
16
+ import type { IExampleUser } from "../../types/IExampleUser";
17
+
18
+ interface IUserSettingsProps {
19
+ setCurrentUser: React.Dispatch<React.SetStateAction<IExampleUser | undefined>>;
20
+ currentUser: IExampleUser;
21
+ isProd: boolean;
22
+ }
23
+
24
+ export default function UserSettings(props: IUserSettingsProps) {
25
+ const useStyles = makeStyles({
26
+ headerRight: {
27
+ display: 'flex',
28
+ alignItems: 'center',
29
+ gap: '16px'
30
+ },
31
+ userName: {
32
+ '@media (max-width: 768px)': {
33
+ display: 'none'
34
+ },
35
+ color: props.isProd ? tokens.colorNeutralForeground1 : tokens.colorNeutralForegroundOnBrand,
36
+ },
37
+ userProfile: {
38
+ display: 'flex',
39
+ alignItems: 'center',
40
+ gap: '8px',
41
+ padding: '8px',
42
+ borderRadius: tokens.borderRadiusMedium,
43
+ cursor: 'pointer',
44
+ ':hover': {
45
+ backgroundColor: tokens.colorNeutralBackground1Hover
46
+ }
47
+ },
48
+ menuTrigger: {
49
+ border: 'none',
50
+ background: 'none',
51
+ padding: 0,
52
+ cursor: 'pointer',
53
+ borderRadius: tokens.borderRadiusMedium,
54
+ ':focus': {
55
+ outline: 'none'
56
+ }
57
+ },
58
+ userInfo: {
59
+ padding: '12px 16px',
60
+ display: 'flex',
61
+ flexDirection: 'column',
62
+ gap: '4px'
63
+ },
64
+ userEmail: {
65
+ fontSize: tokens.fontSizeBase200,
66
+ color: tokens.colorNeutralForeground3
67
+ },
68
+ mobileUserButton: {
69
+ backgroundColor: 'transparent',
70
+ border: 'none',
71
+ '@media (min-width: 769px)': {
72
+ display: 'none'
73
+ }
74
+ },
75
+ desktopUserMenu: {
76
+ '@media (max-width: 768px)': {
77
+ display: 'none'
78
+ }
79
+ }
80
+ });
81
+ const styles = useStyles();
82
+
83
+ const navigate = useNavigate();
84
+
85
+ const handleMobileUserClick = () => {
86
+ navigate('/user-settings');
87
+ };
88
+
89
+ return (
90
+ <div className={styles.headerRight}>
91
+ <button
92
+ className={`${styles.userProfile} ${styles.mobileUserButton}`}
93
+ onClick={handleMobileUserClick}
94
+ aria-label="Go to user settings"
95
+ >
96
+ <Avatar
97
+ name={props.currentUser.Name}
98
+ size={32}
99
+ color="colorful"
100
+ image={{ src: props.currentUser.PhotoURL }}
101
+ />
102
+ <Text className={styles.userName}>{props.currentUser.Name}</Text>
103
+ </button>
104
+
105
+ <div className={styles.desktopUserMenu}>
106
+ <Menu>
107
+ <MenuTrigger disableButtonEnhancement>
108
+ <button className={styles.menuTrigger} aria-label="User menu">
109
+ <div className={styles.userProfile}>
110
+ <Avatar
111
+ name={props.currentUser.Name}
112
+ size={32}
113
+ color="colorful"
114
+ image={{ src: props.currentUser.PhotoURL }}
115
+ />
116
+ <Text className={styles.userName}>{props.currentUser.Name}</Text>
117
+ </div>
118
+ </button>
119
+ </MenuTrigger>
120
+ <MenuPopover>
121
+ <MenuList>
122
+ <div className={styles.userInfo}>
123
+ <Text weight="semibold">{props.currentUser.Name}</Text>
124
+ <Text className={styles.userEmail}>{props.currentUser.UserId}</Text>
125
+ </div>
126
+ <MenuDivider />
127
+ <MenuItem
128
+ icon={<DarkTheme20Regular />}
129
+ onClick={() => {
130
+ const updatedUser = {
131
+ ...props.currentUser,
132
+ IsDarkTheme: !props.currentUser.IsDarkTheme
133
+ };
134
+ props.setCurrentUser(updatedUser);
135
+ }}
136
+ >
137
+ Switch to {props.currentUser.IsDarkTheme === true ? 'Light Mode' : 'Dark Mode'}
138
+ </MenuItem>
139
+ <MenuDivider />
140
+ </MenuList>
141
+ </MenuPopover>
142
+ </Menu>
143
+ </div>
144
+ </div>
145
+ );
146
+ }
@@ -0,0 +1,86 @@
1
+ import { Cube20Regular, Home20Regular, Shield20Regular } from "@fluentui/react-icons";
2
+ import type { IModuleConfig, IUserPermissions, INavItem } from "../types";
3
+ import type { IExampleUser } from "../../../types/IExampleUser";
4
+
5
+ export const ExampleNavItems = {
6
+ Home: "ExampleHome",
7
+ Admin: "ExampleAdmin",
8
+ AdminConfiguration: "ExampleAdminConfiguration",
9
+ };
10
+
11
+ export interface IExamplePermissions extends IUserPermissions {
12
+ isDeveloper: boolean;
13
+ isAppManager: boolean;
14
+ isPowerUser: boolean;
15
+ isSuperUser: boolean;
16
+ isDevOrAppManager: boolean;
17
+ }
18
+
19
+ const resolveExamplePermissions = (user: IExampleUser): IExamplePermissions => {
20
+ const isDeveloper = user.IsDeveloper;
21
+ const isAppManager = user.IsAppManager;
22
+ const isPowerUser = user.IsPowerUser;
23
+ const isSuperUser = user.IsSuperUser;
24
+ const isDevOrAppManager = isDeveloper || isAppManager;
25
+
26
+ return {
27
+ isDeveloper,
28
+ isAppManager,
29
+ isPowerUser,
30
+ isSuperUser,
31
+ isDevOrAppManager,
32
+ };
33
+ };
34
+
35
+ const canViewExampleAdmin = (permissions: IUserPermissions): boolean => {
36
+ const perms = permissions as IExamplePermissions;
37
+ return perms.isDevOrAppManager;
38
+ };
39
+
40
+ export const ExampleConfig: IModuleConfig = {
41
+ id: "Example",
42
+ label: "Example",
43
+ icon: Cube20Regular,
44
+ href: "/example",
45
+ navItems: ExampleNavItems,
46
+ resolvePermissions: resolveExamplePermissions,
47
+ canViewAdmin: canViewExampleAdmin,
48
+ getAdminNavItems: (activeModule: string, permissions: IUserPermissions) => {
49
+ const perms = permissions as IExamplePermissions;
50
+ const navItems: INavItem[] = [];
51
+ if (perms.isDevOrAppManager) {
52
+ const activeModuleLower = activeModule.toLowerCase();
53
+ navItems.push({
54
+ id: ExampleNavItems.AdminConfiguration,
55
+ label: "Configuration",
56
+ icon: Shield20Regular,
57
+ href: `/${activeModuleLower}/configuration`,
58
+ });
59
+ }
60
+ return navItems;
61
+ },
62
+ getRegularNavItems: (activeModule: string, _permissions: IUserPermissions) => {
63
+ const activeModuleLower = activeModule.toLowerCase();
64
+ const navItems: INavItem[] = [
65
+ {
66
+ id: ExampleNavItems.Home,
67
+ label: "Home",
68
+ icon: Home20Regular,
69
+ href: `/${activeModuleLower}`,
70
+ },
71
+ ];
72
+ return navItems;
73
+ },
74
+ getMobileAdminNavItem: (activeModule: string, canViewAdmin: boolean) => {
75
+ return canViewAdmin
76
+ ? [
77
+ {
78
+ id: ExampleNavItems.Admin,
79
+ label: "Configuration",
80
+ icon: Shield20Regular,
81
+ href: `/${activeModule.toLowerCase()}/configuration`,
82
+ },
83
+ ]
84
+ : [];
85
+ },
86
+ };
@@ -0,0 +1,8 @@
1
+ import { ExampleConfig } from "./ExampleConfig";
2
+ import type { IModuleRegistry } from "../types";
3
+
4
+ export { ExampleConfig, ExampleNavItems } from "./ExampleConfig";
5
+
6
+ export const moduleRegistry: IModuleRegistry = {
7
+ IExample: ExampleConfig,
8
+ };
@@ -0,0 +1,48 @@
1
+ import React from "react";
2
+ import type { IExampleUser } from "../../types/IExampleUser";
3
+
4
+ export interface INavItemBase {
5
+ id: string;
6
+ }
7
+
8
+ export interface INavLinkItem extends INavItemBase {
9
+ label: string;
10
+ icon: React.ElementType;
11
+ href: string;
12
+ isDivider?: false;
13
+ }
14
+
15
+ export interface INavDividerItem extends INavItemBase {
16
+ isDivider: true;
17
+ }
18
+
19
+ export type INavItem = INavLinkItem | INavDividerItem;
20
+
21
+ export interface IModuleNavItems {
22
+ [key: string]: string;
23
+ }
24
+
25
+ export interface IUserPermissions {
26
+ [key: string]: boolean | undefined;
27
+ }
28
+
29
+ export type PermissionResolver = (user: IExampleUser) => IUserPermissions;
30
+
31
+ export type CanViewAdminResolver = (permissions: IUserPermissions) => boolean;
32
+
33
+ export interface IModuleConfig {
34
+ id: string;
35
+ label: string;
36
+ icon: React.ElementType;
37
+ href: string;
38
+ navItems: IModuleNavItems;
39
+ resolvePermissions: PermissionResolver;
40
+ canViewAdmin: CanViewAdminResolver;
41
+ getAdminNavItems: (activeModule: string, permissions: IUserPermissions) => INavItem[];
42
+ getRegularNavItems: (activeModule: string, permissions: IUserPermissions) => INavItem[];
43
+ getMobileAdminNavItem: (activeModule: string, canViewAdmin: boolean) => INavItem[];
44
+ }
45
+
46
+ export interface IModuleRegistry {
47
+ [moduleId: string]: IModuleConfig;
48
+ }