Skip to content

Commit

Permalink
Theming changes for the new architecture
Browse files Browse the repository at this point in the history
  • Loading branch information
vivek-harness committed Nov 26, 2024
1 parent 65fd025 commit 4090f2e
Show file tree
Hide file tree
Showing 16 changed files with 1,320 additions and 21 deletions.
11 changes: 8 additions & 3 deletions apps/gitness/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ import {
RepoSettingsPlaceholderPage,
SandboxSettings,
SettingsAccountPage,
SettingsProjectNav,
ThemeProvider
SettingsProjectNav
} from '@harnessio/views'

import { FileEditor } from './components/FileEditor'
Expand Down Expand Up @@ -65,6 +64,8 @@ import { CreateNewUserContainer } from './pages/user-management/create-new-user-
import { UserManagementPageContainer } from './pages/user-management/user-management-container'
import { CreateWebhookContainer } from './pages/webhooks/create-webhook-container'
import RepoWebhooksListPage from './pages/webhooks/repo-webhook-list'
import { ProfileSettingsThemePage } from './pages/profile-settings/profile-settings-theme-page'
import { ThemeProvider } from './framework/context/ThemeContext'

const BASE_URL_PREFIX = `${window.apiUrl || ''}/api/v1`

Expand Down Expand Up @@ -109,6 +110,10 @@ export default function App() {
{
path: ':spaceId/repos',
element: <ReposListPage />
},
{
path: 'theme',
element: <ProfileSettingsThemePage />
}
]
},
Expand Down Expand Up @@ -484,7 +489,7 @@ export default function App() {

return (
<AppProvider>
<ThemeProvider defaultTheme="dark">
<ThemeProvider>
<QueryClientProvider client={queryClient}>
<TooltipProvider>
<ExitConfirmProvider>
Expand Down
56 changes: 56 additions & 0 deletions apps/gitness/src/framework/context/ThemeContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { useEffect } from 'react'
import { ColorType, ContrastType, FullTheme, ModeType, ThemeState } from '@harnessio/ui/components'
import { create } from 'zustand'
import { persist } from 'zustand/middleware'

const useThemeStore = create<ThemeState>()(
persist(
set => ({
theme: 'system-std-std' as FullTheme, // Default theme
setTheme: (newTheme: FullTheme) => set({ theme: newTheme })
}),
{
name: 'canary-ui-theme' // LocalStorage key
}
)
)

export const useTheme = () => {
const { theme, setTheme } = useThemeStore(state => ({ theme: state.theme, setTheme: state.setTheme }))
return { theme, setTheme }
}

export function ThemeProvider({ children }: { children: React.ReactNode }) {
const { theme, setTheme } = useThemeStore(state => ({
theme: state.theme,
setTheme: state.setTheme
}))

useEffect(() => {
const root = window.document.documentElement

const getMediaQuery = () => window.matchMedia('(prefers-color-scheme: dark)')
const [mode, color, contrast] = theme.split('-') as [ModeType, ColorType, ContrastType]

// Compute the effective theme based on system preference if set to "system"
const effectiveMode = mode === ModeType.System ? (getMediaQuery().matches ? ModeType.Dark : ModeType.Light) : mode
const effectiveTheme: FullTheme = `${effectiveMode}-${color}-${contrast}`

root.className = '' // Clear existing classes
root.classList.add(effectiveTheme) // Apply the computed theme class

const updateSystemTheme = () => {
if (theme.startsWith(ModeType.System)) {
setTheme(`${getMediaQuery().matches ? ModeType.Dark : ModeType.Light}-${color}-${contrast}` as FullTheme)
}
}

getMediaQuery().addEventListener('change', updateSystemTheme)

return () => {
getMediaQuery().removeEventListener('change', updateSystemTheme)
}
}, [theme, setTheme])

return <>{children}</>
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'

import { Avatar, AvatarFallback, AvatarImage, Button, ButtonGroup, Icon, Input, Spacer, Text } from '@harnessio/canary'
import { FormFieldSet, getInitials, ModeToggle, SandboxLayout, SkeletonList } from '@harnessio/views'
import { FormFieldSet, getInitials, SandboxLayout, SkeletonList } from '@harnessio/views'

const profileSchema = z.object({
name: z.string().min(1, { message: 'Please provide your name' }),
Expand Down Expand Up @@ -217,11 +217,6 @@ const SettingsAccountGeneralPage: React.FC<SettingsAccountGeneralPageProps> = ({
)}
</FormFieldSet.ControlGroup>

<FormFieldSet.ControlGroup>
<FormFieldSet.Label>Theme</FormFieldSet.Label>
<ModeToggle />
</FormFieldSet.ControlGroup>

{error && error.type === 'profile' && (
<>
<Spacer size={2} />
Expand All @@ -238,8 +233,7 @@ const SettingsAccountGeneralPage: React.FC<SettingsAccountGeneralPageProps> = ({
<Button
size="sm"
type="submit"
disabled={!isProfileValid || isUpdatingUser || !Object.keys(profileDirtyFields).length}
>
disabled={!isProfileValid || isUpdatingUser || !Object.keys(profileDirtyFields).length}>
{isUpdatingUser ? 'Updating...' : 'Update profile'}
</Button>
) : (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Spacer, Text, ThemeSelector } from '@harnessio/ui/components'
import { SandboxLayout } from '@harnessio/ui/views'
import { useTheme } from '../../framework/context/ThemeContext'

const ProfileSettingsThemePage: React.FC = () => {
return (
<SandboxLayout.Main hasLeftPanel hasHeader hasSubHeader>
<SandboxLayout.Content>
<Spacer size={10} />
<Text size={5} weight={'medium'}>
Theme Selector
</Text>
<Spacer size={6} />
<ThemeSelector useTheme={useTheme} />
</SandboxLayout.Content>
</SandboxLayout.Main>
)
}

export { ProfileSettingsThemePage }
4 changes: 4 additions & 0 deletions packages/ui/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ export * from './alert-dialog'
export * from './popover'
export * from './avatar'
export * from './navbar'
export * from './label'
export * from './select'
export * from './theme-selector'
export * from './theme-selector/types'
export * as ListActions from './list-actions'
export * as ListPagination from './list-pagination'
export * as SearchBox from './search-box'
Expand Down
27 changes: 27 additions & 0 deletions packages/ui/src/components/label.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import * as React from 'react'

import { cn } from '@utils/cn'
import * as LabelPrimitive from '@radix-ui/react-label'
import { cva, type VariantProps } from 'class-variance-authority'

const labelVariants = cva('leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70', {
variants: {
variant: {
default: 'text-sm font-light',
sm: 'text-xs font-light'
}
},
defaultVariants: {
variant: 'default'
}
})

const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>
>(({ className, variant, ...props }, ref) => (
<LabelPrimitive.Root ref={ref} className={cn(labelVariants({ variant }), className)} {...props} />
))
Label.displayName = LabelPrimitive.Root.displayName

export { Label }
4 changes: 2 additions & 2 deletions packages/ui/src/components/navbar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,13 @@ export const Navbar = ({
if (!showNavbar) return null

return (
<NavbarSkeleton.Root className="fixed inset-y-0 left-0 z-50 overflow-hidden max-md:hidden">
<NavbarSkeleton.Root className="bg-background fixed inset-y-0 left-0 z-50 overflow-hidden max-md:hidden">
<NavbarSkeleton.Header>
<NavbarProjectChooser.Root
logo={
<Link className="flex items-center gap-1.5" to="/">
<Icon name="harness" size={18} className="text-foreground-1" />
<Icon name="harness-logo-text" width={65} height={15} className="mb-0.5 text-foreground-1" />
<Icon name="harness-logo-text" width={65} height={15} className="text-foreground-1 mb-0.5" />
</Link>
}
/>
Expand Down
136 changes: 136 additions & 0 deletions packages/ui/src/components/select.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import * as React from 'react'

import { cn } from '@utils/cn'
import { CaretSortIcon, CheckIcon, ChevronDownIcon, ChevronUpIcon } from '@radix-ui/react-icons'
import * as SelectPrimitive from '@radix-ui/react-select'

const Select = SelectPrimitive.Root

const SelectGroup = SelectPrimitive.Group

const SelectValue = SelectPrimitive.Value

const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> & { iconClassName?: string }
>(({ className, children, iconClassName, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
'border-input ring-offset-background placeholder:text-muted-foreground focus:ring-ring flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border bg-transparent px-3 py-2 text-sm shadow-sm focus:outline-none focus:ring-1 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
className
)}
{...props}>
{children}
<SelectPrimitive.Icon asChild>
<CaretSortIcon className={cn('h-4 w-4 opacity-50', iconClassName)} />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName

const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn('flex cursor-default items-center justify-center py-1', className)}
{...props}>
<ChevronUpIcon />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName

const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn('flex cursor-default items-center justify-center py-1', className)}
{...props}>
<ChevronDownIcon />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName

const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = 'popper', ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border shadow-md',
position === 'popper' &&
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
className
)}
position={position}
{...props}>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
'p-1',
position === 'popper' &&
'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]'
)}>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName

const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label ref={ref} className={cn('px-2 py-1.5 text-sm font-semibold', className)} {...props} />
))
SelectLabel.displayName = SelectPrimitive.Label.displayName

const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
'focus:bg-accent focus:text-accent-foreground relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className
)}
{...props}>
<span className="absolute right-2 flex size-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName

const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator ref={ref} className={cn('bg-muted -mx-1 my-1 h-px', className)} {...props} />
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName

export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton
}
48 changes: 48 additions & 0 deletions packages/ui/src/components/theme-selector/color-select.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {
Label,
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectSeparator,
SelectTrigger,
SelectValue
} from '..'
import { ColorType, ContrastType, FullTheme, ModeType } from './types'

export function ColorSelect({
setTheme,
mode,
color,
contrast
}: {
setTheme: (theme: FullTheme) => void
color: ColorType
mode: ModeType
contrast: ContrastType
}) {
return (
<div>
<Label className="text-xs">Color</Label>
<Select
value={color}
onValueChange={(color: ColorType) => {
setTheme(`${mode}-${color}-${contrast}`)
}}>
<SelectTrigger>
<SelectValue placeholder="Select a color theme" />
</SelectTrigger>
<SelectContent defaultValue={color}>
<SelectItem value={ColorType.Standard}>Standard</SelectItem>
<SelectSeparator />
<SelectGroup>
<SelectLabel>Vision Assistive</SelectLabel>
<SelectItem value={ColorType.Tritanopia}>Tritanopia</SelectItem>
<SelectItem value={ColorType.ProtanopiaAndDeuteranopia}>Protanopia & Deuteranopia</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
)
}
Loading

0 comments on commit 4090f2e

Please sign in to comment.