From 34b65a75b175222cd290f9402115771aa7030074 Mon Sep 17 00:00:00 2001 From: Martimex Date: Wed, 7 Feb 2024 18:16:17 +0100 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9C=A8=20feat:=20add=20light/dark=20them?= =?UTF-8?q?e=20switch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Include a new switch button that allows users to preview their README files for both light and dark themes (default theme is dark one). This feature aims to reduce the issue with black colored icons (mostly devicons / simple icons) being barely visible on the dark canvas. Also it is a nice UX enhancement for those users who simply prefer light theming. Fixes: #44 --- src/app/events/handles/canvas.ts | 4 ++++ src/components/canvas/index.tsx | 25 ++++++++++++++++++++++--- src/components/canvas/styles.ts | 23 +++++++++++++++++++---- src/contexts/canvas.tsx | 17 +++++++++++++++-- src/types/events.ts | 1 + 5 files changed, 61 insertions(+), 9 deletions(-) diff --git a/src/app/events/handles/canvas.ts b/src/app/events/handles/canvas.ts index 3d55e75..22c9d86 100644 --- a/src/app/events/handles/canvas.ts +++ b/src/app/events/handles/canvas.ts @@ -45,6 +45,10 @@ class CanvasHandleEvents extends BaseEventHandle { currentSection = (sectionId: string) => { this.emit(Events.CANVAS_SET_CURRENT_SECTION, sectionId); }; + + switchTheme = (theme: boolean) => { + this.emit(Events.CANVAS_SWITCH_THEME, theme); + }; } export { CanvasHandleEvents }; diff --git a/src/components/canvas/index.tsx b/src/components/canvas/index.tsx index e95daae..fee17ce 100644 --- a/src/components/canvas/index.tsx +++ b/src/components/canvas/index.tsx @@ -4,6 +4,8 @@ import { Reorder } from 'framer-motion'; import { Trash as TrashIcon, Check as CheckIcon, + Moon as MoonIcon, + Sun as SunIcon, X as CloseIcon, } from '@styled-icons/feather'; @@ -24,7 +26,7 @@ import { CanvasErrorFallback } from './error'; const Canvas = () => { const { extensions } = useExtensions(); - const { sections, currentSection, previewMode } = useCanvas(); + const { sections, currentSection, previewMode, lightTheme } = useCanvas(); const [hasError, setHasError] = useState(false); const sectionIds = sections.map(section => section.id); @@ -41,9 +43,26 @@ const Canvas = () => { + + + events.canvas.switchTheme(lightTheme)} + variant="success" + > + {lightTheme ? : } + + + + {hasSection && !previewMode && ( - + { onChange={setHasError} > {previewMode && ( - + ` - ${({ theme, fullHeight }) => css` + ${({ theme, fullHeight, isLightTheme }) => css` padding: ${theme.spacings.xlarge}; border-radius: ${theme.border.radius}; border-width: ${theme.border.width}; @@ -16,6 +21,10 @@ export const Container = styled.div` padding-right: ${theme.spacings.small}; height: ${fullHeight ? '100%' : 'auto'}; + background: ${isLightTheme && '#eee'}; + color: ${isLightTheme && theme.colors.bg}; + transition: color 0.25s linear, background 0.25s linear; + &::-webkit-scrollbar { width: 0.8rem; overflow: hidden; @@ -31,8 +40,8 @@ export const Container = styled.div` `} `; -export const Wrapper = styled.div` - ${({ theme }) => css` +export const Wrapper = styled.div` + ${({ theme, isLeftAligned }) => css` width: 3rem; position: absolute; display: flex; @@ -44,7 +53,7 @@ export const Wrapper = styled.div` color: ${theme.colors.text}; top: ${theme.spacings.medium}; - left: 0; + left: ${isLeftAligned ? '0%' : '100%'}; transform: translateX(-50%); transition: 0.3s; @@ -75,6 +84,12 @@ const buttonModifiers = { color: ${theme.colors.secondary}; } `, + + info: (theme: DefaultTheme) => css` + &:hover { + color: ${theme.colors.primary}; + } + `, }; export const Button = styled.button` diff --git a/src/contexts/canvas.tsx b/src/contexts/canvas.tsx index 4cdf88a..c1bf55d 100644 --- a/src/contexts/canvas.tsx +++ b/src/contexts/canvas.tsx @@ -18,6 +18,7 @@ type CanvasContextData = { sections: CanvasSection[]; currentSection?: CanvasSection; previewMode: boolean; + lightTheme: boolean; }; type CanvasProviderProps = { @@ -32,6 +33,7 @@ const CanvasProvider = ({ children }: CanvasProviderProps) => { [] ); + const [lightTheme, setLightTheme] = useState(false); const [currentSection, setCurrentSection] = useState(); const [previewTemplate, setPreviewTemplate] = useState([]); @@ -135,6 +137,10 @@ const CanvasProvider = ({ children }: CanvasProviderProps) => { const handleClearCanvas = () => setSections([]); + const handleSwitchTheme = () => { + setLightTheme(!lightTheme); + }; + useEffect(() => { // Canvas events @@ -144,6 +150,7 @@ const CanvasProvider = ({ children }: CanvasProviderProps) => { events.on(Events.CANVAS_REORDER_SECTIONS, handleReorderSections); events.on(Events.CANVAS_DUPLICATE_SECTION, handleDuplicateSection); events.on(Events.CANVAS_CLEAR_SECTIONS, handleClearCanvas); + events.on(Events.CANVAS_SWITCH_THEME, handleSwitchTheme); return () => { events.off(Events.CANVAS_EDIT_SECTION, handleEditSection); @@ -152,8 +159,9 @@ const CanvasProvider = ({ children }: CanvasProviderProps) => { events.off(Events.CANVAS_REORDER_SECTIONS, handleReorderSections); events.off(Events.CANVAS_DUPLICATE_SECTION, handleDuplicateSection); events.off(Events.CANVAS_CLEAR_SECTIONS, handleClearCanvas); + events.off(Events.CANVAS_SWITCH_THEME, handleSwitchTheme); }; - }, [sections, currentSection]); + }, [sections, currentSection, lightTheme]); useEffect(() => { // Canvas events @@ -182,7 +190,12 @@ const CanvasProvider = ({ children }: CanvasProviderProps) => { return ( {children} diff --git a/src/types/events.ts b/src/types/events.ts index 34975d9..433684b 100644 --- a/src/types/events.ts +++ b/src/types/events.ts @@ -6,6 +6,7 @@ export enum Events { CANVAS_REORDER_SECTIONS = 'canvas.section.reorder', CANVAS_DUPLICATE_SECTION = 'canvas.section.duplicate', CANVAS_CLEAR_SECTIONS = 'canvas.clear', + CANVAS_SWITCH_THEME = 'canvas.switchTheme', TEMPLATE_USE = 'template.use', TEMPLATE_PREVIEW = 'template.preview', From 3ee8e02d9faa49c7e920a0e34b7e6f3a65337f2b Mon Sep 17 00:00:00 2001 From: Martimex Date: Fri, 6 Sep 2024 12:01:16 +0200 Subject: [PATCH 2/2] =?UTF-8?q?=E2=9C=A8=20feat:=20update=20light/dark=20t?= =?UTF-8?q?heme=20switch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Include a new switch button that allows users to preview their README files for both light and dark themes (default theme is dark one). This feature aims to reduce the issue with black colored icons (mostly devicons / simple icons) being barely visible on the dark canvas. Also it is a nice UX enhancement for those users who simply prefer light theming. Fixes: #44 --- src/app/events/handles/app.ts | 17 ++++++++++ src/app/events/handles/canvas.ts | 4 --- src/app/events/handles/index.ts | 2 ++ src/components/canvas/index.tsx | 58 +++++++++++++++++++------------- src/components/canvas/styles.ts | 17 +++------- src/contexts/canvas.tsx | 11 +----- src/pages/_app.tsx | 25 ++++++++++++-- src/styles/global.ts | 10 ++++++ src/styles/themes/default.ts | 2 ++ src/styles/themes/index.ts | 17 +++++++++- src/styles/themes/light.ts | 50 +++++++++++++++++++++++++++ src/types/events.ts | 2 +- src/types/styled-components.d.ts | 43 +++++++++++++++++++++-- 13 files changed, 201 insertions(+), 57 deletions(-) create mode 100644 src/app/events/handles/app.ts create mode 100644 src/styles/themes/light.ts diff --git a/src/app/events/handles/app.ts b/src/app/events/handles/app.ts new file mode 100644 index 0000000..0d5f474 --- /dev/null +++ b/src/app/events/handles/app.ts @@ -0,0 +1,17 @@ +import { themes } from "styles" +import { Events } from 'types'; + +import { BaseEventHandle } from './base'; + +class AppHandleEvents extends BaseEventHandle { + constructor() { + super(); + + } + + theme = (theme: keyof typeof themes) => { + this.emit(Events.APP_SET_THEME, theme); + } +} + +export { AppHandleEvents }; \ No newline at end of file diff --git a/src/app/events/handles/canvas.ts b/src/app/events/handles/canvas.ts index 22c9d86..3d55e75 100644 --- a/src/app/events/handles/canvas.ts +++ b/src/app/events/handles/canvas.ts @@ -45,10 +45,6 @@ class CanvasHandleEvents extends BaseEventHandle { currentSection = (sectionId: string) => { this.emit(Events.CANVAS_SET_CURRENT_SECTION, sectionId); }; - - switchTheme = (theme: boolean) => { - this.emit(Events.CANVAS_SWITCH_THEME, theme); - }; } export { CanvasHandleEvents }; diff --git a/src/app/events/handles/index.ts b/src/app/events/handles/index.ts index f97aeb4..b0bae3f 100644 --- a/src/app/events/handles/index.ts +++ b/src/app/events/handles/index.ts @@ -1,3 +1,4 @@ +import { AppHandleEvents } from './app'; import { CanvasHandleEvents } from './canvas'; import { ContextMenuHandleEvents } from './context-menu'; import { SettingsHandleEvents } from './settings'; @@ -8,6 +9,7 @@ import { TemplateHandleEvents } from './template'; import { ExtensionsHandleEvents } from './extensions'; class Handles { + app = new AppHandleEvents(); canvas = new CanvasHandleEvents(); contextmenu = new ContextMenuHandleEvents(); settings = new SettingsHandleEvents(); diff --git a/src/components/canvas/index.tsx b/src/components/canvas/index.tsx index fee17ce..0764de3 100644 --- a/src/components/canvas/index.tsx +++ b/src/components/canvas/index.tsx @@ -1,4 +1,5 @@ import { MouseEvent, useMemo, useState } from 'react'; +import { useTheme } from 'styled-components'; import { Reorder } from 'framer-motion'; import { @@ -26,9 +27,11 @@ import { CanvasErrorFallback } from './error'; const Canvas = () => { const { extensions } = useExtensions(); - const { sections, currentSection, previewMode, lightTheme } = useCanvas(); + const { sections, currentSection, previewMode } = useCanvas(); const [hasError, setHasError] = useState(false); + const currentTheme = useTheme(); + const sectionIds = sections.map(section => section.id); const hasSection = !!sections.length; @@ -38,49 +41,56 @@ const Canvas = () => { !previewMode && events.contextmenu.open(ContextMenus.SECTION, e); }; + const getNextThemeName = (themeName: string) => { + if(themeName === 'dark') return 'light'; + return 'dark'; + } + + const handleSetTheme = () => { + events.app.theme(getNextThemeName(currentTheme.NAME)); + } + return ( - + events.canvas.switchTheme(lightTheme)} - variant="success" + aria-label={`Preview: ${getNextThemeName(currentTheme.NAME)} mode`} + onClick={() => handleSetTheme()} > - {lightTheme ? : } + {getNextThemeName(currentTheme.NAME) === 'dark' ? : } + + {hasSection && !previewMode && ( + + + + + + + + )} - {hasSection && !previewMode && ( - - - - - - - - )} - } onChange={setHasError} > {previewMode && ( - + ` - ${({ theme, fullHeight, isLightTheme }) => css` + ${({ theme, fullHeight }) => css` padding: ${theme.spacings.xlarge}; border-radius: ${theme.border.radius}; border-width: ${theme.border.width}; @@ -21,9 +16,7 @@ export const Container = styled.div` padding-right: ${theme.spacings.small}; height: ${fullHeight ? '100%' : 'auto'}; - background: ${isLightTheme && '#eee'}; - color: ${isLightTheme && theme.colors.bg}; - transition: color 0.25s linear, background 0.25s linear; + border: 2px solid ${theme.colors.border}; &::-webkit-scrollbar { width: 0.8rem; @@ -40,8 +33,8 @@ export const Container = styled.div` `} `; -export const Wrapper = styled.div` - ${({ theme, isLeftAligned }) => css` +export const Wrapper = styled.div` + ${({ theme }) => css` width: 3rem; position: absolute; display: flex; @@ -53,7 +46,7 @@ export const Wrapper = styled.div` color: ${theme.colors.text}; top: ${theme.spacings.medium}; - left: ${isLeftAligned ? '0%' : '100%'}; + left: 0; transform: translateX(-50%); transition: 0.3s; diff --git a/src/contexts/canvas.tsx b/src/contexts/canvas.tsx index c1bf55d..cd1f072 100644 --- a/src/contexts/canvas.tsx +++ b/src/contexts/canvas.tsx @@ -18,7 +18,6 @@ type CanvasContextData = { sections: CanvasSection[]; currentSection?: CanvasSection; previewMode: boolean; - lightTheme: boolean; }; type CanvasProviderProps = { @@ -33,7 +32,6 @@ const CanvasProvider = ({ children }: CanvasProviderProps) => { [] ); - const [lightTheme, setLightTheme] = useState(false); const [currentSection, setCurrentSection] = useState(); const [previewTemplate, setPreviewTemplate] = useState([]); @@ -137,10 +135,6 @@ const CanvasProvider = ({ children }: CanvasProviderProps) => { const handleClearCanvas = () => setSections([]); - const handleSwitchTheme = () => { - setLightTheme(!lightTheme); - }; - useEffect(() => { // Canvas events @@ -150,7 +144,6 @@ const CanvasProvider = ({ children }: CanvasProviderProps) => { events.on(Events.CANVAS_REORDER_SECTIONS, handleReorderSections); events.on(Events.CANVAS_DUPLICATE_SECTION, handleDuplicateSection); events.on(Events.CANVAS_CLEAR_SECTIONS, handleClearCanvas); - events.on(Events.CANVAS_SWITCH_THEME, handleSwitchTheme); return () => { events.off(Events.CANVAS_EDIT_SECTION, handleEditSection); @@ -159,9 +152,8 @@ const CanvasProvider = ({ children }: CanvasProviderProps) => { events.off(Events.CANVAS_REORDER_SECTIONS, handleReorderSections); events.off(Events.CANVAS_DUPLICATE_SECTION, handleDuplicateSection); events.off(Events.CANVAS_CLEAR_SECTIONS, handleClearCanvas); - events.off(Events.CANVAS_SWITCH_THEME, handleSwitchTheme); }; - }, [sections, currentSection, lightTheme]); + }, [sections, currentSection]); useEffect(() => { // Canvas events @@ -194,7 +186,6 @@ const CanvasProvider = ({ children }: CanvasProviderProps) => { sections: canvas, currentSection, previewMode, - lightTheme: lightTheme, }} > {children} diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 77eb154..8752f41 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -1,4 +1,4 @@ -import { MouseEvent, useEffect } from 'react'; +import React, { MouseEvent, useEffect, useState, useRef } from 'react'; import Head from 'next/head'; import { AppProps } from 'next/app'; @@ -10,22 +10,41 @@ import { config, events } from 'app'; import { ContextMenu, Modal } from 'components'; import { Features } from 'features'; -import { theme, GlobalStyles } from 'styles'; +import { themes, GlobalStyles } from 'styles'; +import { Events } from 'types'; const App = ({ Component, pageProps }: AppProps) => { const appUrl = config.general.urls.app; + const [currTheme, setCurrTheme] = useState(themes['dark']); + const isTransition = useRef(false); + const handlePreventRightClick = (e: MouseEvent) => { e.preventDefault(); events.contextmenu.close(); }; + const handleSetTheme = async(e: CustomEvent) => { + document.querySelectorAll(`ul`).forEach(el => el.classList.add('no-animate')); + if(!isTransition.current) { + isTransition.current = true; + setCurrTheme(themes[e.detail]); + document.body.animate([{}], { duration: 450, iterations: 1, direction: 'alternate'}); + Promise.all(document.body.getAnimations().map((animation) => animation.finished)).then(async() => { + return new Promise((resolve) => + resolve(document.querySelectorAll(`ul`).forEach(el => el.classList.remove('no-animate'))) + ).then(() => isTransition.current = false) + }); + }} + useEffect(() => { events.on('contextmenu', handlePreventRightClick); + events.on(Events.APP_SET_THEME, handleSetTheme); return () => { events.on('contextmenu', handlePreventRightClick); + events.off(Events.APP_SET_THEME, handleSetTheme); }; }, []); @@ -34,7 +53,7 @@ const App = ({ Component, pageProps }: AppProps) => { 'Beautify your github profile with this amazing tool, creating the readme your way in a simple and fast way! The best profile readme generator you will find!'; return ( - + {title} diff --git a/src/styles/global.ts b/src/styles/global.ts index 87cbc55..b13bfe4 100644 --- a/src/styles/global.ts +++ b/src/styles/global.ts @@ -15,11 +15,21 @@ const GlobalStyles = createGlobalStyle` html { font-size: 10px; + transition: background-color .45s linear !important; + } + + ul.no-animate li { + -webkit-transition: none !important; + -moz-transition: none !important; + -o-transition: none !important; + transition: none !important; + transform: none !important; } ul, li { list-style: none; + transition: 0 all linear; } a { diff --git a/src/styles/themes/default.ts b/src/styles/themes/default.ts index 6a2bebf..6fdf308 100644 --- a/src/styles/themes/default.ts +++ b/src/styles/themes/default.ts @@ -1,4 +1,6 @@ const defaultTheme = { + NAME: 'dark', + grid: { container: '104rem', }, diff --git a/src/styles/themes/index.ts b/src/styles/themes/index.ts index 6a65b73..ccdcf6b 100644 --- a/src/styles/themes/index.ts +++ b/src/styles/themes/index.ts @@ -1 +1,16 @@ -export { defaultTheme as theme } from './default'; +import { DefaultTheme } from 'styled-components'; +import { defaultTheme } from './default'; +import { lightTheme } from './light'; + +const themes: ThemeObject = { + dark: defaultTheme, + light: lightTheme +} + +type ThemeObject = { + [key: string]: DefaultTheme; + dark: DefaultTheme, + light: DefaultTheme +} + +export { themes } \ No newline at end of file diff --git a/src/styles/themes/light.ts b/src/styles/themes/light.ts new file mode 100644 index 0000000..5d45a2f --- /dev/null +++ b/src/styles/themes/light.ts @@ -0,0 +1,50 @@ +const lightTheme = { + NAME: 'light', + + grid: { + container: '104rem', + }, + + border: { + width: '1px', + radius: '6px', + }, + + font: { + family: + "-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji'", + + weights: { + normal: 400, + bold: 600, + }, + + sizes: { + xsmall: '1.2rem', + small: '1.4rem', + medium: '1.6rem', + large: '2.0rem', + xlarge: '2.6rem', + }, + }, + + colors: { + primary: '#1f75d7', + secondary: '#3dd264', + tertiary: '#f78166', + border: '#30363d', + text: '#0d1117', // #0d1117 #c9d1d9 + bg: '#eee', + error: '#f85149', + }, + + spacings: { + xsmall: '0.8rem', + small: '1.2rem', + medium: '1.6rem', + large: '2.0rem', + xlarge: '2.4rem', + }, +} + +export { lightTheme }; \ No newline at end of file diff --git a/src/types/events.ts b/src/types/events.ts index 433684b..34ca1b7 100644 --- a/src/types/events.ts +++ b/src/types/events.ts @@ -6,7 +6,7 @@ export enum Events { CANVAS_REORDER_SECTIONS = 'canvas.section.reorder', CANVAS_DUPLICATE_SECTION = 'canvas.section.duplicate', CANVAS_CLEAR_SECTIONS = 'canvas.clear', - CANVAS_SWITCH_THEME = 'canvas.switchTheme', + APP_SET_THEME = 'app.set.theme', TEMPLATE_USE = 'template.use', TEMPLATE_PREVIEW = 'template.preview', diff --git a/src/types/styled-components.d.ts b/src/types/styled-components.d.ts index 6995809..1b0ff26 100644 --- a/src/types/styled-components.d.ts +++ b/src/types/styled-components.d.ts @@ -5,5 +5,44 @@ type Theme = typeof theme; declare module 'styled-components' { // eslint-disable-next-line @typescript-eslint/no-empty-interface - export interface DefaultTheme extends Theme {} -} + export interface DefaultTheme extends Theme { + NAME: string, + grid: { + container: string + }, + border: { + width: string, + radius: string + }, + font: { + family: string, + weights: { + normal: number, + bold: number + }, + sizes: { + xsmall: string, + small: string, + medium: string, + large: string, + xlarge: string + } + }, + colors: { + primary: string, + secondary: string, + tertiary: string, + border: string, + text: string, + bg: string, + error: string + }, + spacings: { + xsmall: string, + small: string, + medium: string, + large: string, + xlarge: string + } + } +}; \ No newline at end of file