@taruvi/navkit 0.0.10 → 0.0.11

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.
@@ -1,7 +1,10 @@
1
1
  {
2
2
  "permissions": {
3
3
  "allow": [
4
- "Bash(npm run dev:*)"
4
+ "Bash(npm run dev:*)",
5
+ "WebSearch",
6
+ "Bash(npm run build:*)",
7
+ "Bash(npm install:*)"
5
8
  ],
6
9
  "deny": [],
7
10
  "ask": []
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taruvi/navkit",
3
- "version": "0.0.10",
3
+ "version": "0.0.11",
4
4
  "main": "src/App.tsx",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -16,22 +16,35 @@
16
16
  "@fortawesome/free-regular-svg-icons": ">=6 <7",
17
17
  "@fortawesome/free-solid-svg-icons": ">=6 <7",
18
18
  "@fortawesome/react-fontawesome": ">=0.2 <1",
19
+ "@mui/icons-material": ">=5 <8",
20
+ "@mui/material": ">=5 <8",
19
21
  "@taruvi/sdk": ">=1 <2",
20
- "react": ">=18 <20",
21
- "@mui/material": ">=5 <8"
22
+ "react": ">=18 <20"
22
23
  },
23
24
  "devDependencies": {
25
+ "@emotion/react": "^11.14.0",
26
+ "@emotion/styled": "^11.14.1",
24
27
  "@eslint/js": "^9.36.0",
28
+ "@fortawesome/fontawesome-svg-core": "^7.1.0",
29
+ "@fortawesome/free-regular-svg-icons": "^7.1.0",
30
+ "@fortawesome/free-solid-svg-icons": "^7.1.0",
31
+ "@fortawesome/react-fontawesome": "^3.1.1",
32
+ "@mui/material": "^7.3.6",
33
+ "@taruvi/sdk": "^1.1.7",
25
34
  "@types/node": "^24.6.0",
26
35
  "@types/react": "^19.1.16",
27
36
  "@vitejs/plugin-react": "^5.0.4",
37
+ "axios": "^1.13.2",
28
38
  "babel-plugin-react-compiler": "^19.1.0-rc.3",
29
39
  "eslint": "^9.36.0",
30
40
  "eslint-plugin-react-hooks": "^5.2.0",
31
41
  "eslint-plugin-react-refresh": "^0.4.22",
32
42
  "globals": "^16.4.0",
43
+ "react": "^19.2.3",
44
+ "react-dom": "^19.2.3",
33
45
  "typescript": "~5.9.3",
34
46
  "typescript-eslint": "^8.45.0",
35
- "vite": "^7.1.7"
47
+ "vite": "^7.1.7",
48
+ "@mui/icons-material": "^7.3.6"
36
49
  }
37
- }
50
+ }
package/src/App.styles.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { colours, dimensions } from './styles/variables'
1
+ import { colours, dimensions, getColors } from './styles/variables'
2
+ import type { ThemeMode } from './styles/variables'
2
3
 
3
4
  export const appStyles = {
4
5
  appBar: {
@@ -63,3 +64,72 @@ export const appStyles = {
63
64
  zIndex: 10,
64
65
  },
65
66
  }
67
+
68
+ export const getAppStyles = (mode: ThemeMode) => {
69
+ const colors = getColors(mode)
70
+ return {
71
+ appBar: {
72
+ height: dimensions.navHeight,
73
+ },
74
+ toolbar: {
75
+ backgroundColor: colors.bg.white,
76
+ },
77
+ leftSection: {
78
+ display: 'flex',
79
+ flexGrow: 1,
80
+ alignItems: 'center',
81
+ justifyContent: 'start',
82
+ gap: '10px',
83
+ },
84
+ logo: {
85
+ maxHeight: '40px',
86
+ cursor: 'pointer',
87
+ },
88
+ defaultLogo: {
89
+ maxHeight: '22px',
90
+ cursor: 'pointer',
91
+ },
92
+ rightSection: {
93
+ display: 'flex',
94
+ gap: '10px',
95
+ alignItems: 'center',
96
+ },
97
+ modal: {
98
+ display: 'flex',
99
+ alignItems: 'center',
100
+ justifyContent: 'center',
101
+ },
102
+ chatCard: {
103
+ width: '90vw',
104
+ maxWidth: '1200px',
105
+ height: '80vh',
106
+ outline: 'none',
107
+ overflow: 'hidden',
108
+ position: 'relative' as const,
109
+ backgroundColor: colors.bg.white,
110
+ },
111
+ chatHeader: {
112
+ position: 'absolute' as const,
113
+ top: 0,
114
+ right: 0,
115
+ zIndex: 1400,
116
+ padding: '8px',
117
+ },
118
+ chatOpenButton: {
119
+ backgroundColor: colors.bg.white,
120
+ '&:hover': {
121
+ backgroundColor: colors.bg.light,
122
+ },
123
+ },
124
+ iconStyle: {
125
+ cursor: 'pointer',
126
+ fontSize: '20px',
127
+ color: colors.text.secondary,
128
+ },
129
+ backdrop: {
130
+ position: "absolute" as const,
131
+ inset: 0,
132
+ zIndex: 10,
133
+ },
134
+ }
135
+ }
package/src/App.tsx CHANGED
@@ -5,13 +5,14 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
5
5
  import { useState } from "react"
6
6
  import { AppLauncher, Profile, ProfileMenu, Shortcuts, ShortcutsMenu, MattermostChat } from "./components"
7
7
  import { Box, Modal, Card } from "@mui/material"
8
- import { appStyles } from "./App.styles"
8
+ import { getAppStyles } from "./App.styles"
9
9
  import { NavkitProvider, useNavigation } from "./NavkitContext"
10
10
  import defaultLogo from "./assets/taruvi-logo.png"
11
11
  import type { Client } from "@taruvi/sdk"
12
12
 
13
13
  const NavkitContent = () => {
14
- const { isUserAuthenticated, siteSettings, userData, appsList, jwtToken } = useNavigation()
14
+ const { isUserAuthenticated, siteSettings, userData, appsList, jwtToken, themeMode } = useNavigation()
15
+ const styles = getAppStyles(themeMode)
15
16
  const [showAppLauncher, setShowAppLauncher] = useState<boolean>(false)
16
17
  const [showProfileMenu, setShowProfileMenu] = useState<boolean>(false)
17
18
  const [showShortcutsMenu, setShowShortcutsMenu] = useState<boolean>(false)
@@ -21,7 +22,7 @@ const NavkitContent = () => {
21
22
  <>
22
23
  {(showAppLauncher || showProfileMenu || showShortcutsMenu) && (
23
24
  <div
24
- style={appStyles.backdrop}
25
+ style={styles.backdrop}
25
26
  onClick={() => {
26
27
  setShowAppLauncher(false)
27
28
  setShowProfileMenu(false)
@@ -29,21 +30,21 @@ const NavkitContent = () => {
29
30
  }}
30
31
  />
31
32
  )}
32
- <AppBar position="static" sx={appStyles.appBar}>
33
- <Toolbar sx={appStyles.toolbar}>
34
- <Box component={"div"} sx={appStyles.leftSection}>
33
+ <AppBar position="static" sx={styles.appBar}>
34
+ <Toolbar sx={styles.toolbar}>
35
+ <Box component={"div"} sx={styles.leftSection}>
35
36
  {isUserAuthenticated && appsList.length > 0 &&
36
37
  <IconButton onClick={() => setShowAppLauncher(!showAppLauncher)}>
37
- <FontAwesomeIcon icon={["fas", "th"]} style={appStyles.iconStyle} />
38
+ <FontAwesomeIcon icon={["fas", "th"]} style={styles.iconStyle} />
38
39
  </IconButton>
39
40
  }
40
41
  <img
41
- style={(siteSettings?.logo && siteSettings.logo !== '') ? appStyles.logo : appStyles.defaultLogo}
42
+ style={(siteSettings?.logo && siteSettings.logo !== '') ? styles.logo : styles.defaultLogo}
42
43
  src={(siteSettings?.logo && siteSettings.logo !== '') ? siteSettings.logo : defaultLogo}
43
44
  onClick={() => window.location.href = siteSettings.frontendUrl || '/desk'}
44
45
  />
45
46
  </Box>
46
- <Box component={'div'} sx={appStyles.rightSection}>
47
+ <Box component={'div'} sx={styles.rightSection}>
47
48
  <Shortcuts
48
49
  showChat={setShowChat}
49
50
  onMenuToggle={() => setShowShortcutsMenu(!showShortcutsMenu)}
@@ -77,13 +78,13 @@ const NavkitContent = () => {
77
78
  <Modal
78
79
  open={showChat}
79
80
  onClose={() => setShowChat(false)}
80
- sx={appStyles.modal}
81
+ sx={styles.modal}
81
82
  >
82
- <Card sx={appStyles.chatCard}>
83
- <Box sx={appStyles.chatHeader}>
83
+ <Card sx={styles.chatCard}>
84
+ <Box sx={styles.chatHeader}>
84
85
  <IconButton
85
86
  onClick={() => window.open(siteSettings.mattermostUrl, '_blank')}
86
- sx={appStyles.chatOpenButton}
87
+ sx={styles.chatOpenButton}
87
88
  >
88
89
  <FontAwesomeIcon icon={["fas", "external-link-alt"]} style={{ fontSize: '10px' }} />
89
90
  </IconButton>
@@ -101,9 +102,14 @@ const NavkitContent = () => {
101
102
  )
102
103
  }
103
104
 
104
- const Navkit = ({ client }: { client: Client }) => {
105
+ interface NavkitProps {
106
+ client: Client
107
+ getTheme?: (theme: 'light' | 'dark') => void
108
+ }
109
+
110
+ const Navkit = ({ client, getTheme }: NavkitProps) => {
105
111
  return (
106
- <NavkitProvider client={client}>
112
+ <NavkitProvider client={client} onThemeChange={getTheme}>
107
113
  <NavkitContent />
108
114
  </NavkitProvider>
109
115
  )
@@ -1,13 +1,25 @@
1
1
  import { createContext, startTransition, useCallback, useContext, useEffect, useLayoutEffect, useRef, useState } from 'react'
2
2
  import type { AppData, NavigationContextType, NavkitProviderProps, SiteSettings, UserData } from './types'
3
+ import type { ThemeMode } from './styles/variables'
3
4
  import { User, Settings, Auth } from "@taruvi/sdk"
4
5
  import { library } from '@fortawesome/fontawesome-svg-core'
5
6
  import { fas } from '@fortawesome/free-solid-svg-icons'
6
7
  import { far } from '@fortawesome/free-regular-svg-icons'
7
8
 
9
+ const THEME_STORAGE_KEY = 'navkit-theme-mode'
10
+
11
+ const getInitialTheme = (): ThemeMode => {
12
+ if (typeof window !== 'undefined') {
13
+ const stored = localStorage.getItem(THEME_STORAGE_KEY)
14
+ if (stored === 'dark' || stored === 'light') return stored
15
+ if (window.matchMedia('(prefers-color-scheme: dark)').matches) return 'dark'
16
+ }
17
+ return 'light'
18
+ }
19
+
8
20
  export const NavigationContext = createContext<NavigationContextType | undefined>(undefined)
9
21
 
10
- export const NavkitProvider = ({ children, client }: NavkitProviderProps) => {
22
+ export const NavkitProvider = ({ children, client, onThemeChange }: NavkitProviderProps) => {
11
23
  const auth = new Auth(client)
12
24
 
13
25
  const user = useRef<any>(null)
@@ -25,6 +37,15 @@ export const NavkitProvider = ({ children, client }: NavkitProviderProps) => {
25
37
  const [userData, setUserData] = useState<UserData | null>(null)
26
38
  const [jwtToken, setJwtToken] = useState<string>('')
27
39
  const [isUserAuthenticated, setIsUserAuthenticated] = useState<boolean>(false)
40
+ const [themeMode, setThemeMode] = useState<ThemeMode>(getInitialTheme)
41
+
42
+ const toggleTheme = useCallback(() => {
43
+ setThemeMode(prev => {
44
+ const newMode = prev === 'light' ? 'dark' : 'light'
45
+ localStorage.setItem(THEME_STORAGE_KEY, newMode)
46
+ return newMode
47
+ })
48
+ }, [])
28
49
 
29
50
  const checkIfDesk = async () => {
30
51
  const fetchedSettings = await settings?.current?.get?.()
@@ -119,8 +140,13 @@ export const NavkitProvider = ({ children, client }: NavkitProviderProps) => {
119
140
  }
120
141
  }, [getData])
121
142
 
143
+ // Notify parent app of theme changes
144
+ useEffect(() => {
145
+ onThemeChange?.(themeMode)
146
+ }, [themeMode, onThemeChange])
147
+
122
148
  return (
123
- <NavigationContext.Provider value={{ navigateToUrl, isDesk, appsList, userData, siteSettings: siteSettings.current, jwtToken, isUserAuthenticated, client, auth }}>
149
+ <NavigationContext.Provider value={{ navigateToUrl, isDesk, appsList, userData, siteSettings: siteSettings.current, jwtToken, isUserAuthenticated, client, auth, themeMode, toggleTheme }}>
124
150
  {children}
125
151
  </NavigationContext.Provider>
126
152
  )
@@ -1,4 +1,5 @@
1
- import { colours, spacing, shadows, borderRadius, typography, zIndex, dimensions } from '../../styles/variables'
1
+ import { colours, spacing, shadows, borderRadius, typography, zIndex, dimensions, getColors } from '../../styles/variables'
2
+ import type { ThemeMode } from '../../styles/variables'
2
3
 
3
4
  export const appLauncherStyles = {
4
5
  container: {
@@ -57,3 +58,64 @@ export const appLauncherStyles = {
57
58
  letterSpacing: typography.letterSpacing.default,
58
59
  },
59
60
  }
61
+
62
+ export const getAppLauncherStyles = (mode: ThemeMode) => {
63
+ const colors = getColors(mode)
64
+ return {
65
+ container: {
66
+ position: 'absolute' as const,
67
+ top: 60,
68
+ left: { xs: '2.5vw', sm: '10vw', md: '15vw', lg: 40 },
69
+ width: { xs: '95vw', sm: '80vw', md: '70vw', lg: '560px' },
70
+ maxWidth: '800px',
71
+ maxHeight: { xs: 'calc(100vh - 80px)', md: '335px' },
72
+ borderRadius: borderRadius.default,
73
+ boxShadow: shadows.dropdown,
74
+ p: spacing.lg,
75
+ backgroundColor: colors.bg.white,
76
+ zIndex: zIndex.overlay,
77
+ },
78
+ scrollContainer: {
79
+ mt: spacing.md,
80
+ maxHeight: { xs: '300px', md: '220px' },
81
+ overflowY: 'auto' as const,
82
+ },
83
+ appItem: {
84
+ display: 'flex',
85
+ flexDirection: 'column' as const,
86
+ alignItems: 'center',
87
+ justifyContent: 'center',
88
+ p: spacing.xs,
89
+ borderRadius: borderRadius.default,
90
+ cursor: 'pointer',
91
+ transition: 'all 0.2s ease',
92
+ '&:hover': {
93
+ backgroundColor: colors.bg.light,
94
+ },
95
+ },
96
+ iconContainer: {
97
+ width: 48,
98
+ height: 48,
99
+ display: 'flex',
100
+ alignItems: 'center',
101
+ justifyContent: 'center',
102
+ borderRadius: borderRadius.default,
103
+ border: `1px solid ${colors.border.light}`,
104
+ mb: spacing.sm,
105
+ },
106
+ iconStyle: {
107
+ fontSize: dimensions.iconSize.md,
108
+ color: colors.text.secondary,
109
+ },
110
+ appName: {
111
+ fontWeight: typography.weights.regular,
112
+ color: colors.text.secondary,
113
+ fontSize: typography.sizes.xs,
114
+ textAlign: 'center' as const,
115
+ lineHeight: 1.3,
116
+ maxWidth: '100%',
117
+ wordWrap: 'break-word' as const,
118
+ letterSpacing: typography.letterSpacing.default,
119
+ },
120
+ }
121
+ }
@@ -5,11 +5,12 @@ import { Card, Box, Grid, Typography } from "@mui/material"
5
5
  import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
6
6
  import type { IconName } from "@fortawesome/fontawesome-svg-core"
7
7
  import { useNavigation } from "../../NavkitContext"
8
- import { appLauncherStyles } from "./AppLauncher.styles"
8
+ import { getAppLauncherStyles } from "./AppLauncher.styles"
9
9
 
10
10
 
11
11
  const AppLauncher = () => {
12
- const { navigateToUrl, appsList } = useNavigation()
12
+ const { navigateToUrl, appsList, themeMode } = useNavigation()
13
+ const styles = getAppLauncherStyles(themeMode)
13
14
  const [filteredApps, setFilteredApps] = useState<AppData[]>(appsList || [])
14
15
 
15
16
  const handleAppClick = (app: AppData) => {
@@ -25,29 +26,29 @@ const AppLauncher = () => {
25
26
  }, [appsList])
26
27
 
27
28
  return (
28
- <Card sx={appLauncherStyles.container}>
29
+ <Card sx={styles.container}>
29
30
  <Search appsList={appsList} onSearchChange={handleSearchChange} />
30
31
  {filteredApps && filteredApps.length > 0 && (
31
- <Box sx={appLauncherStyles.scrollContainer}>
32
+ <Box sx={styles.scrollContainer}>
32
33
  <Grid container spacing={1}>
33
34
  {filteredApps.map((app) => {
34
35
  return (
35
36
  <Grid item xs={3} key={app.id}>
36
37
  <Box
37
- sx={appLauncherStyles.appItem}
38
+ sx={styles.appItem}
38
39
  onClick={() => handleAppClick(app)}
39
40
  >
40
41
  <Box
41
- sx={appLauncherStyles.iconContainer}
42
+ sx={styles.iconContainer}
42
43
  >
43
44
  <FontAwesomeIcon
44
45
  icon={["fas", app.icon.replace('fa-', '') as IconName]}
45
- style={appLauncherStyles.iconStyle}
46
+ style={styles.iconStyle}
46
47
  />
47
48
  </Box>
48
49
  <Typography
49
50
  variant="body2"
50
- sx={appLauncherStyles.appName}
51
+ sx={styles.appName}
51
52
  >
52
53
  {app.appname}
53
54
  </Typography>
@@ -1,4 +1,5 @@
1
- import { colours, spacing, shadows, borderRadius, typography, zIndex, dimensions } from '../../styles/variables'
1
+ import { colours, spacing, shadows, borderRadius, typography, zIndex, dimensions, getColors } from '../../styles/variables'
2
+ import type { ThemeMode } from '../../styles/variables'
2
3
 
3
4
  export const profileStyles = {
4
5
  container: {
@@ -57,3 +58,64 @@ export const profileStyles = {
57
58
  color: colours.text.secondary,
58
59
  },
59
60
  }
61
+
62
+ export const getProfileStyles = (mode: ThemeMode) => {
63
+ const colors = getColors(mode)
64
+ return {
65
+ container: {
66
+ position: 'relative' as const,
67
+ },
68
+ trigger: {
69
+ display: 'flex',
70
+ alignItems: 'center',
71
+ gap: spacing.sm,
72
+ ml: spacing.md,
73
+ cursor: 'pointer',
74
+ },
75
+ avatar: {
76
+ width: dimensions.avatarSize,
77
+ height: dimensions.avatarSize,
78
+ bgcolor: colors.bg.avatar,
79
+ color: colors.text.primary,
80
+ fontWeight: typography.weights.semibold,
81
+ fontSize: typography.sizes.sm,
82
+ },
83
+ name: {
84
+ color: colors.text.primary,
85
+ fontSize: typography.sizes.sm,
86
+ fontWeight: typography.weights.regular,
87
+ letterSpacing: typography.letterSpacing.default,
88
+ display: { xs: 'none', md: 'block' },
89
+ },
90
+ menu: {
91
+ position: 'absolute' as const,
92
+ top: 60,
93
+ right: 0,
94
+ width: 200,
95
+ borderRadius: borderRadius.default,
96
+ boxShadow: shadows.dropdown,
97
+ backgroundColor: colors.bg.white,
98
+ zIndex: zIndex.overlay,
99
+ py: 1,
100
+ },
101
+ menuItem: {
102
+ display: 'flex',
103
+ alignItems: 'center',
104
+ gap: spacing.md,
105
+ px: spacing.md,
106
+ py: spacing.sm,
107
+ '&:hover': {
108
+ backgroundColor: colors.bg.light,
109
+ },
110
+ },
111
+ menuItemText: {
112
+ color: colors.text.secondary,
113
+ fontSize: typography.sizes.sm,
114
+ fontWeight: typography.weights.regular,
115
+ },
116
+ iconStyle: {
117
+ fontSize: dimensions.iconSize.sm,
118
+ color: colors.text.secondary,
119
+ },
120
+ }
121
+ }
@@ -1,5 +1,6 @@
1
1
  import { Box, Avatar, Typography } from '@mui/material'
2
- import { profileStyles } from './Profile.styles'
2
+ import { getProfileStyles } from './Profile.styles'
3
+ import { useNavigation } from '../../NavkitContext'
3
4
  import nopfp from "../../assets/nopfp.png"
4
5
 
5
6
  interface UserData {
@@ -18,15 +19,17 @@ interface ProfileProps {
18
19
 
19
20
  const Profile = (props: ProfileProps) => {
20
21
  const { userData, onClick } = props
22
+ const { themeMode } = useNavigation()
23
+ const styles = getProfileStyles(themeMode)
21
24
 
22
25
  return (
23
- <Box sx={profileStyles.trigger} onClick={onClick}>
26
+ <Box sx={styles.trigger} onClick={onClick}>
24
27
  <Avatar
25
28
  src={userData.pfp || nopfp}
26
- sx={profileStyles.avatar}
29
+ sx={styles.avatar}
27
30
  >
28
31
  </Avatar>
29
- <Typography sx={profileStyles.name}>
32
+ <Typography sx={styles.name}>
30
33
  {userData.first_name} {userData.last_name}
31
34
  </Typography>
32
35
  </Box>
@@ -1,10 +1,11 @@
1
1
  import { Card, MenuItem, Typography } from '@mui/material'
2
2
  import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
3
- import { profileStyles } from './Profile.styles'
3
+ import { getProfileStyles } from './Profile.styles'
4
4
  import { useNavigation } from '../../NavkitContext'
5
5
 
6
6
  const ProfileMenu = () => {
7
- const { navigateToUrl, auth } = useNavigation()
7
+ const { navigateToUrl, auth, themeMode } = useNavigation()
8
+ const styles = getProfileStyles(themeMode)
8
9
 
9
10
  const handlePreferences = () => {
10
11
  navigateToUrl('preferences', true)
@@ -15,23 +16,23 @@ const ProfileMenu = () => {
15
16
  }
16
17
 
17
18
  return (
18
- <Card sx={profileStyles.menu}>
19
- <MenuItem onClick={handlePreferences} sx={profileStyles.menuItem}>
19
+ <Card sx={styles.menu}>
20
+ <MenuItem onClick={handlePreferences} sx={styles.menuItem}>
20
21
  <FontAwesomeIcon
21
22
  icon={["fas", "gear"]}
22
- style={profileStyles.iconStyle}
23
+ style={styles.iconStyle}
23
24
  />
24
- <Typography sx={profileStyles.menuItemText}>
25
+ <Typography sx={styles.menuItemText}>
25
26
  Preferences
26
27
  </Typography>
27
28
  </MenuItem>
28
29
 
29
- <MenuItem onClick={handleLogout} sx={profileStyles.menuItem}>
30
+ <MenuItem onClick={handleLogout} sx={styles.menuItem}>
30
31
  <FontAwesomeIcon
31
32
  icon={["fas", "right-from-bracket"]}
32
- style={profileStyles.iconStyle}
33
+ style={styles.iconStyle}
33
34
  />
34
- <Typography sx={profileStyles.menuItemText}>
35
+ <Typography sx={styles.menuItemText}>
35
36
  Logout
36
37
  </Typography>
37
38
  </MenuItem>
@@ -1,3 +1,6 @@
1
+ import { getColors } from '../../styles/variables'
2
+ import type { ThemeMode } from '../../styles/variables'
3
+
1
4
  export const searchStyles = {
2
5
  textField: {
3
6
  '& .MuiOutlinedInput-root': {
@@ -5,3 +8,31 @@ export const searchStyles = {
5
8
  },
6
9
  },
7
10
  }
11
+
12
+ export const getSearchStyles = (mode: ThemeMode) => {
13
+ const colors = getColors(mode)
14
+ return {
15
+ textField: {
16
+ '& .MuiOutlinedInput-root': {
17
+ height: '40px',
18
+ backgroundColor: colors.bg.light,
19
+ '& fieldset': {
20
+ borderColor: colors.border.light,
21
+ },
22
+ '&:hover fieldset': {
23
+ borderColor: colors.text.tertiary,
24
+ },
25
+ },
26
+ '& .MuiOutlinedInput-input': {
27
+ color: colors.text.primary,
28
+ },
29
+ '& .MuiInputBase-input::placeholder': {
30
+ color: colors.text.tertiary,
31
+ opacity: 1,
32
+ },
33
+ },
34
+ iconStyle: {
35
+ color: colors.text.tertiary,
36
+ },
37
+ }
38
+ }
@@ -1,7 +1,8 @@
1
1
  import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
2
2
  import { TextField, InputAdornment } from '@mui/material'
3
3
  import { useState } from 'react'
4
- import { searchStyles } from './Search.styles'
4
+ import { getSearchStyles } from './Search.styles'
5
+ import { useNavigation } from '../../NavkitContext'
5
6
  import type { AppData } from '../../types'
6
7
 
7
8
  interface SearchProps {
@@ -11,6 +12,8 @@ interface SearchProps {
11
12
 
12
13
  const Search = ({ appsList, onSearchChange }: SearchProps) => {
13
14
  const [searchTerm, setSearchTerm] = useState('')
15
+ const { themeMode } = useNavigation()
16
+ const styles = getSearchStyles(themeMode)
14
17
 
15
18
  const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
16
19
  const value = event.target.value
@@ -36,11 +39,11 @@ const Search = ({ appsList, onSearchChange }: SearchProps) => {
36
39
  variant="outlined"
37
40
  value={searchTerm}
38
41
  onChange={handleSearchChange}
39
- sx={searchStyles.textField}
42
+ sx={styles.textField}
40
43
  InputProps={{
41
44
  startAdornment: (
42
45
  <InputAdornment position="start">
43
- <FontAwesomeIcon icon={["fas", "magnifying-glass"]} style={{ color: '#9e9e9e' }} />
46
+ <FontAwesomeIcon icon={["fas", "magnifying-glass"]} style={styles.iconStyle} />
44
47
  </InputAdornment>
45
48
  ),
46
49
  }}
@@ -1,4 +1,5 @@
1
- import { colours, spacing, dimensions } from '../../styles/variables'
1
+ import { colours, spacing, dimensions, getColors } from '../../styles/variables'
2
+ import type { ThemeMode } from '../../styles/variables'
2
3
 
3
4
  export const shortcutsStyles = {
4
5
  desktopContainer: {
@@ -27,3 +28,19 @@ export const shortcutsStyles = {
27
28
  color: colours.text.secondary,
28
29
  },
29
30
  }
31
+
32
+ export const getThemeToggleStyles = (mode: ThemeMode) => {
33
+ const colors = getColors(mode)
34
+ return {
35
+ button: {
36
+ color: colors.text.secondary,
37
+ '&:hover': {
38
+ backgroundColor: colors.bg.light,
39
+ },
40
+ },
41
+ icon: {
42
+ fontSize: dimensions.iconSize.lg,
43
+ color: colors.text.secondary,
44
+ },
45
+ }
46
+ }
@@ -1,7 +1,9 @@
1
1
  import type { AppData } from '../../types'
2
2
  import { Box, Link, IconButton } from '@mui/material'
3
3
  import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
4
- import { shortcutsStyles } from './Shortcuts.styles'
4
+ import DarkModeOutlined from '@mui/icons-material/DarkModeOutlined'
5
+ import LightModeOutlined from '@mui/icons-material/LightModeOutlined'
6
+ import { shortcutsStyles, getThemeToggleStyles } from './Shortcuts.styles'
5
7
  import { useNavigation } from '../../NavkitContext'
6
8
 
7
9
  interface ShortcutsProps {
@@ -13,18 +15,16 @@ interface ShortcutsProps {
13
15
  const Shortcuts = (props: ShortcutsProps) => {
14
16
 
15
17
  const { showChat, onMenuToggle, triggerRef } = props
16
- const { navigateToUrl, siteSettings, isUserAuthenticated } = useNavigation()
18
+ const { navigateToUrl, siteSettings, isUserAuthenticated, themeMode, toggleTheme } = useNavigation()
17
19
  const shortcuts = siteSettings.shortcuts
20
+ const themeToggleStyles = getThemeToggleStyles(themeMode)
18
21
 
19
22
  const handleShortcutClick = (shortcut: AppData) => {
20
23
  if (shortcut.url) {
21
24
  navigateToUrl(shortcut.url, false)
22
25
  }
23
26
  }
24
- if ((!shortcuts || shortcuts.length === 0) && !siteSettings.showChat) {
25
- return null
26
- }
27
-
27
+
28
28
  return (
29
29
  <>
30
30
  {/* Desktop view - inline shortcuts */}
@@ -52,6 +52,13 @@ const Shortcuts = (props: ShortcutsProps) => {
52
52
  />
53
53
  </Link>
54
54
  }
55
+ <IconButton onClick={toggleTheme} sx={themeToggleStyles.button}>
56
+ {themeMode === 'dark' ? (
57
+ <LightModeOutlined sx={themeToggleStyles.icon} />
58
+ ) : (
59
+ <DarkModeOutlined sx={themeToggleStyles.icon} />
60
+ )}
61
+ </IconButton>
55
62
  </Box>
56
63
 
57
64
  {/* Mobile view - trigger button */}
@@ -1,4 +1,5 @@
1
- import { colours, spacing, shadows, borderRadius, typography, zIndex, dimensions } from '../../styles/variables'
1
+ import { colours, spacing, shadows, borderRadius, typography, zIndex, dimensions, getColors } from '../../styles/variables'
2
+ import type { ThemeMode } from '../../styles/variables'
2
3
 
3
4
  export const shortcutsMenuStyles = {
4
5
  menu: {
@@ -32,3 +33,43 @@ export const shortcutsMenuStyles = {
32
33
  color: colours.text.secondary,
33
34
  },
34
35
  }
36
+
37
+ export const getShortcutsMenuStyles = (mode: ThemeMode) => {
38
+ const colors = getColors(mode)
39
+ return {
40
+ menu: {
41
+ position: 'absolute' as const,
42
+ top: 60,
43
+ right: 0,
44
+ width: 200,
45
+ borderRadius: borderRadius.default,
46
+ boxShadow: shadows.dropdown,
47
+ backgroundColor: colors.bg.white,
48
+ zIndex: zIndex.overlay,
49
+ py: 1,
50
+ },
51
+ menuItem: {
52
+ display: 'flex',
53
+ alignItems: 'center',
54
+ gap: spacing.md,
55
+ px: spacing.md,
56
+ py: spacing.sm,
57
+ '&:hover': {
58
+ backgroundColor: colors.bg.light,
59
+ },
60
+ },
61
+ menuItemText: {
62
+ color: colors.text.secondary,
63
+ fontSize: typography.sizes.sm,
64
+ fontWeight: typography.weights.regular,
65
+ },
66
+ iconStyle: {
67
+ fontSize: dimensions.iconSize.sm,
68
+ color: colors.text.secondary,
69
+ },
70
+ themeIcon: {
71
+ fontSize: dimensions.iconSize.sm,
72
+ color: colors.text.secondary,
73
+ },
74
+ }
75
+ }
@@ -1,7 +1,9 @@
1
1
  import { Card, MenuItem, Typography } from '@mui/material'
2
2
  import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
3
+ import DarkModeOutlined from '@mui/icons-material/DarkModeOutlined'
4
+ import LightModeOutlined from '@mui/icons-material/LightModeOutlined'
3
5
  import type { AppData } from '../../types'
4
- import { shortcutsMenuStyles } from './ShortcutsMenu.styles'
6
+ import { getShortcutsMenuStyles } from './ShortcutsMenu.styles'
5
7
  import { useNavigation } from '../../NavkitContext'
6
8
 
7
9
  interface ShortcutsMenuProps {
@@ -10,8 +12,9 @@ interface ShortcutsMenuProps {
10
12
 
11
13
  const ShortcutsMenu = (props: ShortcutsMenuProps) => {
12
14
  const { showChat } = props
13
- const { navigateToUrl, siteSettings, isUserAuthenticated } = useNavigation()
15
+ const { navigateToUrl, siteSettings, isUserAuthenticated, themeMode, toggleTheme } = useNavigation()
14
16
  const shortcuts = siteSettings.shortcuts
17
+ const themedStyles = getShortcutsMenuStyles(themeMode)
15
18
 
16
19
  const handleShortcutClick = (shortcut: AppData) => {
17
20
  if (shortcut.url) {
@@ -19,23 +22,19 @@ const ShortcutsMenu = (props: ShortcutsMenuProps) => {
19
22
  }
20
23
  }
21
24
 
22
- if ((!shortcuts || shortcuts.length === 0) && !siteSettings.showChat) {
23
- return null
24
- }
25
-
26
25
  return (
27
- <Card sx={shortcutsMenuStyles.menu}>
26
+ <Card sx={themedStyles.menu}>
28
27
  {siteSettings.showChat && isUserAuthenticated && (
29
28
  <MenuItem
30
29
  key="chat"
31
30
  onClick={() => showChat(true)}
32
- sx={shortcutsMenuStyles.menuItem}
31
+ sx={themedStyles.menuItem}
33
32
  >
34
33
  <FontAwesomeIcon
35
34
  icon={["far", "comment"] as any}
36
- style={shortcutsMenuStyles.iconStyle}
35
+ style={themedStyles.iconStyle}
37
36
  />
38
- <Typography sx={shortcutsMenuStyles.menuItemText}>
37
+ <Typography sx={themedStyles.menuItemText}>
39
38
  Chat
40
39
  </Typography>
41
40
  </MenuItem>
@@ -44,17 +43,31 @@ const ShortcutsMenu = (props: ShortcutsMenuProps) => {
44
43
  <MenuItem
45
44
  key={shortcut.id}
46
45
  onClick={() => handleShortcutClick(shortcut)}
47
- sx={shortcutsMenuStyles.menuItem}
46
+ sx={themedStyles.menuItem}
48
47
  >
49
48
  <FontAwesomeIcon
50
49
  icon={["far", shortcut.icon] as any}
51
- style={shortcutsMenuStyles.iconStyle}
50
+ style={themedStyles.iconStyle}
52
51
  />
53
- <Typography sx={shortcutsMenuStyles.menuItemText}>
52
+ <Typography sx={themedStyles.menuItemText}>
54
53
  {shortcut.appname}
55
54
  </Typography>
56
55
  </MenuItem>
57
56
  ))}
57
+ <MenuItem
58
+ key="theme-toggle"
59
+ onClick={toggleTheme}
60
+ sx={themedStyles.menuItem}
61
+ >
62
+ {themeMode === 'dark' ? (
63
+ <LightModeOutlined sx={themedStyles.themeIcon} />
64
+ ) : (
65
+ <DarkModeOutlined sx={themedStyles.themeIcon} />
66
+ )}
67
+ <Typography sx={themedStyles.menuItemText}>
68
+ {themeMode === 'dark' ? 'Light Mode' : 'Dark Mode'}
69
+ </Typography>
70
+ </MenuItem>
58
71
  </Card>
59
72
  )
60
73
  }
@@ -1,6 +1,8 @@
1
1
  // Design Tokens - Centralized design values for consistent theming
2
2
 
3
- export const colours = {
3
+ export type ThemeMode = 'light' | 'dark'
4
+
5
+ export const lightColors = {
4
6
  text: {
5
7
  primary: '#333333',
6
8
  secondary: '#424242',
@@ -16,6 +18,27 @@ export const colours = {
16
18
  },
17
19
  }
18
20
 
21
+ export const darkColors = {
22
+ text: {
23
+ primary: '#f5f5f5',
24
+ secondary: '#e0e0e0',
25
+ tertiary: '#9e9e9e',
26
+ },
27
+ bg: {
28
+ white: '#1e1e1e',
29
+ light: '#2d2d2d',
30
+ avatar: '#424242',
31
+ },
32
+ border: {
33
+ light: '#424242',
34
+ },
35
+ }
36
+
37
+ export const getColors = (mode: ThemeMode) => mode === 'dark' ? darkColors : lightColors
38
+
39
+ // Default colors (light mode) for backwards compatibility
40
+ export const colours = lightColors
41
+
19
42
  export const spacing = {
20
43
  xs: '10px',
21
44
  sm: 1.5,
package/src/types.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { ReactNode } from "react"
2
2
  import type { Client, Auth } from "@taruvi/sdk"
3
+ import type { ThemeMode } from "./styles/variables"
3
4
 
4
5
  export interface AppData {
5
6
  appname: string,
@@ -33,9 +34,12 @@ export interface NavigationContextType {
33
34
  isUserAuthenticated: boolean
34
35
  client: Client
35
36
  auth: Auth
37
+ themeMode: ThemeMode
38
+ toggleTheme: () => void
36
39
  }
37
40
 
38
41
  export interface NavkitProviderProps {
39
42
  children: ReactNode
40
43
  client: Client
44
+ onThemeChange?: (theme: ThemeMode) => void
41
45
  }