diff --git a/public/fonts/roboto-mono.woff2 b/public/fonts/roboto-mono.woff2 new file mode 100644 index 00000000..07cf89e0 Binary files /dev/null and b/public/fonts/roboto-mono.woff2 differ diff --git a/src/shared/util/colors.ts b/src/shared/util/colors.ts index 451f1f09..9f6279dd 100644 --- a/src/shared/util/colors.ts +++ b/src/shared/util/colors.ts @@ -264,6 +264,33 @@ export function getDarkerShade(color: HexColor, offset: number = 20): HexColor { return `#${newRGB.map(c => Math.round(c).toString(16).padStart(2, '0')).join('')}`; } +/** + * Returns a lighter shade of the given hex color by increasing the lightness in HSL color space. + * + * @param color - The hexadecimal color value to lighten. + * @param offset - The percentage to increase the lightness by (default is 20). + * @returns The lighter shade of the given hex color. + * @throws If the provided color is not a valid hex color. + */ +export function getLighterShade(color: HexColor, offset: number = 20): HexColor { + const rgb = hexToRGB(color); + if (!rgb) { + throw new Error('color: Invalid hex.'); + } + + // Convert to HSL + const [h, s, l] = hexToHSL(color); + + // Increase lightness by offset percentage, ensuring it doesn't go above 100 + const newL = Math.min(100, l + offset); + + // Convert back to RGB + const newRGB = hslToRGB([h, s, newL]); + + // Convert to hex + return `#${newRGB.map(c => Math.round(c).toString(16).padStart(2, '0')).join('')}`; +} + /** * Get next unused color in a tailwind colorway for a given schedule * diff --git a/src/shared/util/tests/colors.test.ts b/src/shared/util/tests/colors.test.ts index 69f54976..6bd47f6a 100644 --- a/src/shared/util/tests/colors.test.ts +++ b/src/shared/util/tests/colors.test.ts @@ -1,4 +1,4 @@ -import { hexToHSL, isValidHexColor } from '@shared/util/colors'; +import { getLighterShade, hexToHSL, isValidHexColor } from '@shared/util/colors'; import { describe, expect, it } from 'vitest'; describe('hexToHSL', () => { @@ -79,3 +79,34 @@ describe('isValidHexColor', () => { expect(isValidHexColor('')).toBe(false); }); }); + +describe('getLighterShade', () => { + it('should lighten a color by default offset (20%)', () => { + const result = getLighterShade('#BF5700'); + expect(result).toBe('#ff8624'); + }); + + it('should lighten black correctly', () => { + const result = getLighterShade('#000000'); + expect(result).toBe('#333333'); + }); + + it('should not exceed 100% lightness', () => { + const result = getLighterShade('#FFFFFF', 20); + expect(result).toBe('#ffffff'); + }); + + it('should handle custom offset values', () => { + const result = getLighterShade('#BF5700', 40); + expect(result).toBe('#ffbe8a'); + }); + + it('should maintain hue while increasing lightness', () => { + const result = getLighterShade('#00FF00', 20); // Pure green + expect(result.toLowerCase()).toBe('#66ff66'); + }); + + it('should throw error for invalid hex color', () => { + expect(() => getLighterShade('#GGGGGG')).toThrow('color: Invalid hex.'); + }); +}); diff --git a/src/views/components/calendar/CalendarCourseCellColorPicker/HexColorEditor.tsx b/src/views/components/calendar/CalendarCourseCellColorPicker/HexColorEditor.tsx index 64a06393..e941746a 100644 --- a/src/views/components/calendar/CalendarCourseCellColorPicker/HexColorEditor.tsx +++ b/src/views/components/calendar/CalendarCourseCellColorPicker/HexColorEditor.tsx @@ -29,6 +29,12 @@ export default function HexColorEditor({ hexCode, setHexCode }: HexColorEditorPr const [localHexCode, setLocalHexCode] = React.useState(hexCode); const debouncedSetHexCode = useDebounce((value: string) => setHexCode(value), 500); + React.useEffect(() => { + if (hexCode !== localHexCode) { + setLocalHexCode(hexCode); + } + }, [hexCode]); + React.useEffect(() => { debouncedSetHexCode(localHexCode); @@ -48,7 +54,7 @@ export default function HexColorEditor({ hexCode, setHexCode }: HexColorEditorPr setLocalHexCode(e.target.value)} /> diff --git a/src/views/hooks/useSchedules.ts b/src/views/hooks/useSchedules.ts index 2d9937f0..08293dcd 100644 --- a/src/views/hooks/useSchedules.ts +++ b/src/views/hooks/useSchedules.ts @@ -1,7 +1,7 @@ import { UserScheduleStore } from '@shared/storage/UserScheduleStore'; import type { HexColor } from '@shared/types/Color'; import { UserSchedule } from '@shared/types/UserSchedule'; -import { getColorwayFromColor, getCourseColors, getDarkerShade } from '@shared/util/colors'; +import { getColorwayFromColor, getCourseColors, getDarkerShade, getLighterShade } from '@shared/util/colors'; import { useEffect, useState } from 'react'; let schedulesCache: UserSchedule[] = []; @@ -150,7 +150,12 @@ export async function updateCourseColors(courseID: number, primaryColor: HexColo secondaryColor = colorFromWay; } catch (e) { - secondaryColor = getDarkerShade(primaryColor, 80); + secondaryColor = getDarkerShade(primaryColor, 20); + + // if primaryColor is too dark, get lighter shade instead + if (secondaryColor === '#000000') { + secondaryColor = getLighterShade(primaryColor, 35); + } } updatedCourse.colors.primaryColor = primaryColor; diff --git a/src/views/styles/fonts.module.scss b/src/views/styles/fonts.module.scss index 0c0f140f..c1724300 100644 --- a/src/views/styles/fonts.module.scss +++ b/src/views/styles/fonts.module.scss @@ -15,7 +15,15 @@ font-style: normal; } +@font-face { + font-family: 'Roboto Mono Local'; + src: url('@public/fonts/roboto-mono.woff2') format('woff2'); + font-display: swap; + font-style: normal; +} + @import url('https://fonts.googleapis.com/css2?family=Roboto+Flex:wght@100..1000&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Roboto+Mono&display=swap'); $medium_size: 16px; diff --git a/unocss.config.ts b/unocss.config.ts index 5893b0c0..073cf74e 100644 --- a/unocss.config.ts +++ b/unocss.config.ts @@ -55,6 +55,7 @@ export default defineConfig({ provider: 'none', fonts: { sans: ['Roboto Flex', 'Roboto Flex Local'], + mono: ['Roboto Mono', 'Roboto Mono Local'], }, }), ],