Skip to content

Commit

Permalink
Add useTheme and fix ChipSet
Browse files Browse the repository at this point in the history
  • Loading branch information
Yusef Almamari committed Oct 15, 2024
1 parent 4c0ddec commit c29555b
Show file tree
Hide file tree
Showing 12 changed files with 275 additions and 163 deletions.
42 changes: 20 additions & 22 deletions src/assets/json/colors.json
Original file line number Diff line number Diff line change
@@ -1,28 +1,26 @@
{
"light": {
"yellow": "rgb(251,176,5)",
"orange": "rgb(243,105,68)",
"red": "rgb(223,49,48)",
"pink": "rgb(156,54,181)",
"purple": "rgb(104,65,218)",
"blue": "rgb(19,110,189)",
"cyan": "rgb(19,163,189)",
"green": "rgb(13,166,120)",
"teal": "rgb(13,166,151)",
"gray": "rgb(127,140,159)",
"brown": "rgb(173,105,68)"
"yellow": "hsl(42, 100%, 40%)",
"orange": "hsl(22, 100%, 40%)",
"red": "hsl(0, 100%, 40%)",
"pink": "hsl(288, 100%, 40%)",
"purple": "hsl(255, 100%, 40%",
"blue": "hsl(210, 100%, 40%)",
"cyan": "hsl(190, 100%, 40%)",
"green": "hsl(150, 100%, 40%)",
"gray": "hsl(216, 40%, 40%)",
"brown": "hsl(21, 40%, 40%)"
},
"dark": {
"yellow": "rgb(251,176,5)",
"orange": "rgb(243,105,68)",
"red": "rgb(223,49,48)",
"pink": "rgb(156,54,181)",
"purple": "rgb(104,65,218)",
"blue": "rgb(19,110,189)",
"cyan": "rgb(19,163,189)",
"green": "rgb(13,166,120)",
"teal": "rgb(13,166,151)",
"gray": "rgb(127,140,159)",
"brown": "rgb(173,105,68)"
"yellow": "hsl(42, 90%, 70%)",
"orange": "hsl(22, 90%, 70%)",
"red": "hsl(0, 90%, 70%)",
"pink": "hsl(288, 90%, 70%)",
"purple": "hsl(255, 90%, 70%",
"blue": "hsl(210, 90%, 70%)",
"cyan": "hsl(190, 90%, 70%)",
"green": "hsl(150, 90%, 70%)",
"gray": "hsl(216, 35%, 70%)",
"brown": "hsl(21, 35%, 70%)"
}
}
1 change: 0 additions & 1 deletion src/assets/json/tags.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
{ "label": "Research topic", "color": "purple", "id": "builtin-researchTopic" },
{ "label": "Course project", "color": "cyan", "id": "builtin-courseProject" },
{ "label": "Personal project", "color": "pink", "id": "builtin-personalProject" },
{ "label": "Collaborative", "color": "teal", "id": "builtin-collaborative" },
{ "label": "Archived", "color": "brown", "id": "builtin-archived" },
{ "label": "Discontinued", "color": "gray", "id": "builtin-discontinued" }
]
96 changes: 47 additions & 49 deletions src/components/ui/MaterialComponents.jsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
/* eslint-disable react/jsx-props-no-spreading, prettier/prettier */
/* eslint-disable react/jsx-props-no-spreading */

import React, { forwardRef, useEffect, useId, useImperativeHandle, useRef, useState } from "react";
import { useNavigate } from "react-router-dom";
import { useSelector } from "react-redux";
import { uid } from "../../utils/utils.ts";
import { useMetaThemeColor } from "../../hooks/hooks.tsx";
import { useMetaThemeColor, useTheme } from "../../hooks/hooks.tsx";
import colorValues from "../../assets/json/colors.json";
import { rgbToRgba } from "../../utils/conversionUtils.tsx";
import { hslToHsla } from "../../utils/conversionUtils.tsx";

export function Divider({ className = "", label, ...rest }) {
if (label)
Expand Down Expand Up @@ -179,7 +178,7 @@ export const Switch = forwardRef(function Switch(props, parentRef) {

export function Icon({ name, className = "", ...rest }) {
return (
<md-icon slot="icon" {...rest} class={`align-middle ${className}`}>
<md-icon slot="icon" class={`align-middle ${className}`} {...rest}>
{name}
</md-icon>
);
Expand Down Expand Up @@ -217,14 +216,6 @@ export function OutlinedButton({ className = "", onClick, type = "button", child
);
}

export function TonalButton({ className = "", onClick, type = "button", children, ...rest }) {
return (
<md-filled-tonal-button type={type} class={`px-6 py-2 font-sans ${className}`} onClick={onClick} {...rest}>
{children}
</md-filled-tonal-button>
);
}

export function IconButton({ className = "", onClick, type = "button", name, ...rest }) {
return (
<md-icon-button type={type} class={className} onClick={onClick} {...rest}>
Expand Down Expand Up @@ -445,49 +436,56 @@ export function TopBar({ headline, description, showBackButton = true, options }
);
}

export function ChipSet(props) {
const { chips = [], className = "", ...rest } = props;
const { data: settings } = useSelector((state) => state.settings);
function Chip(props) {
const { className, onClick, start, label, end, selected, color, ...rest } = props;
const [theme] = useTheme();
const id = uid();

return (
<div className={`flex flex-wrap gap-1 ${className}`} {...rest}>
{chips.map((chip) => {
if (!chip || !colorValues[settings.theme]) return undefined;
const targetColor = colorValues[theme][color];

const targetColor = colorValues[settings.theme][chip.color];
function getTextColor() {
if (selected) {
if (theme === "dark") return "white";
return "";
}
return targetColor;
}

const style = {
"--md-filled-tonal-button-container-color": rgbToRgba(targetColor, 0.4),
"--md-filled-tonal-button-container-shape": "10px",
const style = {
color: getTextColor(),
background: selected ? hslToHsla(targetColor, 0.25) : "transparent",
border: `1px solid ${selected ? "transparent" : targetColor}`,
};

"--md-outlined-button-outline-color": targetColor,
"--md-outlined-button-container-shape": "10px",
"--md-outlined-button-label-text-color": targetColor,
};
return (
<div id={id} className={`relative *:rounded-[8px] ${className}`} {...rest}>
<md-focus-ring part="focus-ring" htmlFor={id} aria-hidden />
<md-ripple part="ripple" htmlFor={id} aria-hidden />
<button
className="flex items-center justify-between gap-1 p-[0.4rem] font-sans font-semibold"
style={style}
onClick={onClick}
type="button"
id={id}
>
{start}
{label}
{end}
</button>
</div>
);
}

const chipProps = {
className: "px-0 py-0 ",
style,
...chip,
};
export function ChipSet(props) {
const { chips = [], className = "", ...rest } = props;
const [theme] = useTheme();

if (chip.selected) {
return (
<TonalButton key={uid()} {...chipProps}>
{chip?.start}
{chip?.label}
{chip?.end}
</TonalButton>
);
}
return (
<div className={`flex flex-wrap gap-1 ${className}`} {...rest}>
{chips.map((chip) => {
if (!chip || !colorValues[theme]) return undefined;

return (
<OutlinedButton key={uid()} {...chipProps}>
{chip?.start}
{chip?.label}
{chip?.end}
</OutlinedButton>
);
return <Chip key={uid()} {...chip} />;
})}
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion src/data/store/slices/settingsSlice.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import db from "../../db/dexie/dexie";
import builtinIcons from "../../../assets/json/icons.json";
import builtinTags from "../../../assets/json/tags.json";

const initialState = { data: { theme: "light", tags: builtinTags, icons: builtinIcons }, loadedLocally: false };
const initialState = { data: { theme: "auto", tags: builtinTags, icons: builtinIcons }, loadedLocally: false };

// FIXME..
async function save(newState) {
Expand Down
103 changes: 60 additions & 43 deletions src/hooks/hooks.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { Dispatch, SetStateAction, useCallback, useEffect, useRef, useState } from "react";
import { User } from "firebase/auth";
import { Location, useLocation, useParams } from "react-router-dom";
import { useDispatch, useSelector } from "react-redux";
Expand Down Expand Up @@ -167,40 +167,56 @@ export function useDocumentTitle(
return title;
}

type ThemeType = "auto" | "light" | "dark";

/**
* Returns the current device theme ("dark" or "light") based on the system preferences.
* A custom hook to manage theme mode (light or dark) and respond to system preferences.
*
* @param {CallableFunction} [onChangeCallback] - Optional callback function to be executed when the theme changes.
* @returns {"dark" | "light"} The current device theme.
* @param {CallableFunction} [onChangeCallback=() => undefined] - A callback function that will be triggered when the theme changes.
*
* @example
* // Usage
* const theme = useDeviceTheme((isDarkMode) => {
* console.log(isDarkMode ? "Dark mode is enabled" : "Light mode is enabled");
* });
* console.log("Current theme:", theme);
* @returns {["light" | "dark", Dispatch<SetStateAction<ThemeType>>]} - Returns an array where the first element is the current theme ("light" or "dark") and the second element is a function to set the theme.
*/
export function useDeviceTheme(onChangeCallback: CallableFunction = () => undefined): "dark" | "light" {
export function useTheme(
onChangeCallback: CallableFunction = () => undefined
): ["light" | "dark", Dispatch<SetStateAction<ThemeType>>] {
const [theme, setTheme] = useState<ThemeType>(() => {
const currentClass = document.documentElement.className;
const modeMatch = currentClass.match(/^(light|dark|auto)-mode/);

if (modeMatch) return modeMatch[1] as ThemeType;
return "auto";
});
const [isDarkMode, setIsDarkMode] = useState<boolean>(false);

useEffect(() => {
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
document.documentElement.classList.remove("light-mode", "dark-mode", "auto-mode");
document.documentElement.classList.add(`${theme}-mode`);
}, [theme]);

const handleChange = () => {
setIsDarkMode(mediaQuery.matches);
onChangeCallback(mediaQuery.matches);
};
useEffect(() => {
if (theme !== "auto") {
setIsDarkMode(theme === "dark");
onChangeCallback(theme === "dark");
} else {
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");

handleChange();
const handleChange = () => {
setIsDarkMode(mediaQuery.matches);
onChangeCallback(mediaQuery.matches);
};

mediaQuery.addEventListener("change", handleChange);
handleChange();

return () => {
mediaQuery.removeEventListener("change", handleChange);
};
}, [onChangeCallback]);
mediaQuery.addEventListener("change", handleChange);

return () => {
mediaQuery.removeEventListener("change", handleChange);
};
}
return undefined;
}, [onChangeCallback, theme]);

return isDarkMode ? "dark" : "light";
return [isDarkMode ? "dark" : "light", setTheme];
}

/**
Expand All @@ -217,14 +233,14 @@ export function useMetaThemeColor(initialColor: string): (newColor: string) => v
const [color, setColor] = useState<string>(
initialColor || window.getComputedStyle(document.documentElement).getPropertyValue("background-color")
);
const currentDeviceTheme = useDeviceTheme();
const [currentTheme] = useTheme();

useEffect(() => {
const metaThemeColor = document.querySelector('meta[name="theme-color"]'); // eslint-disable-line quotes
if (metaThemeColor) {
metaThemeColor.setAttribute("content", color);
}
}, [color, currentDeviceTheme]);
}, [color, currentTheme]);

return (newColor) => setColor(newColor);
}
Expand All @@ -249,7 +265,7 @@ export function useOnlineStatus(): boolean {
*
* The returned function allows you to execute a callback function after a specified delay.
*
* @returns {function(callback: () => void, ms?: number): function}
* @returns {(callback: () => void, ms: number) => () => void}
* A function that takes a callback to execute after a delay and an optional delay time in milliseconds.
* The returned function can be called to set the timeout.
*
Expand All @@ -266,8 +282,9 @@ export function useOnlineStatus(): boolean {
* <button onClick={handleClick}>Click me</button>
* );
*/
export function useTimeout() {
const savedCallback = useRef<() => void>();
// eslint-disable-next-line no-unused-vars
export function useTimeout(): (callback: () => void, ms: number) => () => void {
const savedCallback = useRef<(() => void) | undefined>(undefined);

const setTimeoutCallback = (callback: () => void, ms: number = 3000) => {
savedCallback.current = callback;
Expand Down Expand Up @@ -346,23 +363,23 @@ export function useKeyboardShortcuts(keymap: Record<string, ShortcutAction>) {
useEffect(registerShortcuts, [registerShortcuts]);
}

const keyMap = {
enter: "Enter",
ctrl: "Control",
meta: "Meta",
shift: "Shift",
alt: "Alt",
tab: "Tab",
escape: "Escape",
};

function parseKeyCombination(keyCombination) {
const keys = keyCombination.split("+").map((key) => keyMap[key.toLowerCase()] || key);
return new Set(keys);
}

/* eslint-disable no-restricted-syntax */
export function useKeyDown() {
const keyMap = {
enter: "Enter",
ctrl: "Control",
meta: "Meta",
shift: "Shift",
alt: "Alt",
tab: "Tab",
escape: "Escape",
};

function parseKeyCombination(keyCombination) {
const keys = keyCombination.split("+").map((key) => keyMap[key.toLowerCase()] || key);
return new Set(keys);
}

return useCallback(
(keyActions) => (event) => {
for (const [keyCombination, callback] of keyActions) {
Expand Down
Loading

0 comments on commit c29555b

Please sign in to comment.