diff --git a/cspell.config.yaml b/cspell.config.yaml index d901d84..ef31cb3 100644 --- a/cspell.config.yaml +++ b/cspell.config.yaml @@ -1,6 +1,7 @@ words: - antd - iife + - oomol - refreshable - tsup - Vals diff --git a/desktop/main/package.json b/desktop/main/package.json index 4748d4e..5cf7f18 100644 --- a/desktop/main/package.json +++ b/desktop/main/package.json @@ -15,7 +15,9 @@ "electron": "^32.0.1", "eslint": "overridden", "vite": "^5.4.2", - "vite-plugin-electron": "^0.28.8" + "vite-plugin-electron": "^0.28.8", + "value-enhancer": "overridden", + "use-value-enhancer": "overridden" }, "dependencies": { } diff --git a/desktop/main/src/index.ts b/desktop/main/src/index.ts index 00c27f9..e7d9cd7 100644 --- a/desktop/main/src/index.ts +++ b/desktop/main/src/index.ts @@ -7,13 +7,18 @@ const __dirname = path.dirname(__filename); async function createWindow() { const mainWindow = new BrowserWindow({ - width: 1600, - height: 1500, + width: 960, + height: 678, show: false, webPreferences: { preload: path.join(__dirname, "preload.mjs"), nodeIntegration: false, }, + titleBarStyle: "hidden", + trafficLightPosition: { + x: 13, + y: 13, + }, }); mainWindow.once("ready-to-show", () => { diff --git a/desktop/renderer/index.html b/desktop/renderer/index.html index 1c42c0f..4f07e7e 100644 --- a/desktop/renderer/index.html +++ b/desktop/renderer/index.html @@ -8,7 +8,7 @@ -
+
diff --git a/desktop/renderer/package.json b/desktop/renderer/package.json index db3b53c..c66ff89 100644 --- a/desktop/renderer/package.json +++ b/desktop/renderer/package.json @@ -22,8 +22,8 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^6.27.0", - "use-value-enhancer": "^5.0.6", - "value-enhancer": "^5.4.2" + "value-enhancer": "overridden", + "use-value-enhancer": "overridden" }, "devDependencies": { "@types/js-yaml": "^4.0.9", diff --git a/desktop/renderer/src/App.tsx b/desktop/renderer/src/App.tsx index f78f535..2b657dc 100644 --- a/desktop/renderer/src/App.tsx +++ b/desktop/renderer/src/App.tsx @@ -1,6 +1,8 @@ import type { PropsWithChildren } from "react"; import React from "react"; +import { useVal } from "use-value-enhancer"; import { AppContextProvider } from "./components/AppContextProvider"; +import { ThemeProvider } from "./components/ThemeProvider"; import { type AppContext, Routes } from "./routes"; export interface StudioHomeProps { @@ -11,10 +13,16 @@ export const StudioHome = ({ appContext, children, }: PropsWithChildren) => { + const prefersColorScheme = useVal(appContext.settingStore.prefersColorScheme$); + return ( - - {children} + + + {children} + ); }; diff --git a/desktop/renderer/src/components/AntdProvider/index.tsx b/desktop/renderer/src/components/AntdProvider/index.tsx new file mode 100644 index 0000000..a0c3bc3 --- /dev/null +++ b/desktop/renderer/src/components/AntdProvider/index.tsx @@ -0,0 +1,39 @@ +import type { MappingAlgorithm, ThemeConfig } from "antd"; +import type { FC } from "react"; +import { ConfigProvider, theme } from "antd"; + +import React, { useMemo } from "react"; + +const antdDarkTheme: MappingAlgorithm = (seedToken, mapToken) => ({ + ...theme.darkAlgorithm(seedToken, mapToken), + colorBgLayout: "#161B22", +}); + +const antdLightTheme: MappingAlgorithm = seedToken => ({ + ...theme.defaultAlgorithm(seedToken), + colorBgLayout: "#ecf0f7", +}); + +export const AntdProvider: FC<{ + darkMode: boolean; + children: React.ReactNode; +}> = ({ darkMode, children }) => { + const theme: ThemeConfig = useMemo( + () => ({ + token: { + colorPrimary: "#7d7fe9", + }, + algorithm: darkMode ? antdDarkTheme : antdLightTheme, + components: { + Table: { + cellPaddingInlineSM: 4, + cellPaddingBlockSM: 5, + cellFontSizeSM: 12, + }, + }, + }), + [darkMode], + ); + + return {children}; +}; diff --git a/desktop/renderer/src/components/AppearancePicker/AppearancePicker.module.scss b/desktop/renderer/src/components/AppearancePicker/AppearancePicker.module.scss new file mode 100644 index 0000000..2bee4db --- /dev/null +++ b/desktop/renderer/src/components/AppearancePicker/AppearancePicker.module.scss @@ -0,0 +1,56 @@ +.container { + > :global(.ant-radio-group) { + > :global(.ant-radio-wrapper) { + position: relative; + border-radius: 4px; + padding: 4px; + + > :global(span) { + padding: 0; + } + + :global(.ant-radio) { + margin-top: 9px; + } + + &::after { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 75%; + border-radius: 4px; + transition: border-color 0.4s; + } + + &:global(.ant-radio-wrapper-checked), + &:hover, + &:active { + &::after { + border: 2px solid #7d7fe9; + } + } + + &:global(.ant-radio-wrapper-checked) { + &::after { + border: 2px solid #7d7fe9; + } + } + } + + :global(.ant-radio) { + display: none; + } + } +} + +.options { + display: flex; + flex-direction: column; + align-items: center; + + > span { + padding-top: 6px; + } +} diff --git a/desktop/renderer/src/components/AppearancePicker/icons/auto.svg b/desktop/renderer/src/components/AppearancePicker/icons/auto.svg new file mode 100644 index 0000000..f55922d --- /dev/null +++ b/desktop/renderer/src/components/AppearancePicker/icons/auto.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/desktop/renderer/src/components/AppearancePicker/icons/dark.svg b/desktop/renderer/src/components/AppearancePicker/icons/dark.svg new file mode 100644 index 0000000..59bfc68 --- /dev/null +++ b/desktop/renderer/src/components/AppearancePicker/icons/dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/desktop/renderer/src/components/AppearancePicker/icons/light.svg b/desktop/renderer/src/components/AppearancePicker/icons/light.svg new file mode 100644 index 0000000..416740a --- /dev/null +++ b/desktop/renderer/src/components/AppearancePicker/icons/light.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/desktop/renderer/src/components/AppearancePicker/index.tsx b/desktop/renderer/src/components/AppearancePicker/index.tsx new file mode 100644 index 0000000..7d3b6e4 --- /dev/null +++ b/desktop/renderer/src/components/AppearancePicker/index.tsx @@ -0,0 +1,50 @@ +import type { RadioChangeEvent } from "antd"; + +import type { OOMOLPrefersColorScheme } from "../ThemeProvider"; +import { Radio } from "antd"; + +import React from "react"; +import styles from "./AppearancePicker.module.scss"; + +import autoSVG from "./icons/auto.svg"; +import darkSVG from "./icons/dark.svg"; +import lightSVG from "./icons/light.svg"; + +export interface AppearancePickerProps { + defaultValue: OOMOLPrefersColorScheme; + changeAppearance: (event: RadioChangeEvent) => void; +} + +export const AppearancePicker: React.FC = ({ + defaultValue, + changeAppearance, +}) => { + // const t = useTranslate(); + return ( +
+ + +
+ + {/* {t("settings.theme-light")} */} + light +
+
+ +
+ + {/* {t("settings.theme-dark")} */} + dark +
+
+ +
+ + {/* {t("settings.theme-auto")} */} + auto +
+
+
+
+ ); +}; diff --git a/desktop/renderer/src/components/HomeTitleBar/HomeTitleBar.module.scss b/desktop/renderer/src/components/HomeTitleBar/HomeTitleBar.module.scss new file mode 100644 index 0000000..839eafc --- /dev/null +++ b/desktop/renderer/src/components/HomeTitleBar/HomeTitleBar.module.scss @@ -0,0 +1,53 @@ +.container { + height: 100%; + overflow: hidden; + display: flex; + align-items: center; + flex-wrap: nowrap; + box-sizing: border-box; + padding: 0 15px 0 20px; +} + +.title { + font-size: 1em; + font-weight: bold; + user-select: none; +} + +.footer { + -webkit-app-region: no-drag; + + margin-left: auto; + display: flex; + align-items: center; +} + +.publish { + margin-right: 8px; + span { + font-size: 12px; + } +} + +.login-form { + margin-top: 28px; +} + +.login-form-button { + width: 100%; + margin-top: 24px; +} + +.upload-info-box { + width: 100%; + display: flex; + flex-direction: row; +} + +.picture-card { + flex-shrink: 0; +} + +.project-description { + width: 100%; +} diff --git a/desktop/renderer/src/components/HomeTitleBar/index.tsx b/desktop/renderer/src/components/HomeTitleBar/index.tsx new file mode 100644 index 0000000..decfbb3 --- /dev/null +++ b/desktop/renderer/src/components/HomeTitleBar/index.tsx @@ -0,0 +1,62 @@ +import React, { createElement, memo, useMemo } from "react"; + +import { useLocation, useOutletContext } from "react-router-dom"; + +import { useIsomorphicLayoutEffect } from "~/hooks"; + +import type { RouteOutletContext } from "~/typings"; +import styles from "./HomeTitleBar.module.scss"; + +/** + * Set its children to the title bar. + */ +export const TitleBarSetter = /* @__PURE__ */ memo( + ({ children }) => { + const setChild = useOutletContext(); + useIsomorphicLayoutEffect(() => { + setChild(children); + }, [children, setChild]); + return null; + }, +); + +export interface HomeTitleBarProps { + title?: React.ReactNode; + footer?: React.ReactNode; +} + +/** + * A default title bar UI. + */ +export const HomeTitleBarLayout = ({ title, footer }: HomeTitleBarProps) => { + // TODO: i18n + // const t = useTranslate(); + const { pathname } = useLocation(); + const routeName: string | undefined = (/^\/home\/([^/]+)/.exec(pathname) || [ + "", + "", + ])[1]; + const name = useMemo(() => { + if (title) { + return title; + } + return routeName && routeName; + }, [title, routeName]); + + return ( +
+

{name}

+
+ {/* {routeName === "community" && } */} + {footer} +
+
+ ); +}; + +/** + * Set a default UI to the title bar. + */ +export const HomeTitleBar = (props: HomeTitleBarProps) => ( + {createElement(HomeTitleBarLayout, props)} +); diff --git a/desktop/renderer/src/components/ThemeProvider/ThemeProvider.scss b/desktop/renderer/src/components/ThemeProvider/ThemeProvider.scss new file mode 100644 index 0000000..ca795ca --- /dev/null +++ b/desktop/renderer/src/components/ThemeProvider/ThemeProvider.scss @@ -0,0 +1,13 @@ +@import "~/styles/theme.scss"; + +.oomol-theme-root { + @include theme-root; +} + +.oomol-theme-light { + @include theme-light; +} + +.oomol-theme-dark { + @include theme-dark; +} diff --git a/desktop/renderer/src/components/ThemeProvider/index.tsx b/desktop/renderer/src/components/ThemeProvider/index.tsx new file mode 100644 index 0000000..685a0ff --- /dev/null +++ b/desktop/renderer/src/components/ThemeProvider/index.tsx @@ -0,0 +1,77 @@ +import type { PropsWithChildren, ReactElement } from "react"; +import React, { createContext, useContext, useEffect, useMemo, useState } from "react"; + +import { useIsomorphicLayoutEffect } from "~/hooks"; + +import { AntdProvider } from "../AntdProvider"; +import "./ThemeProvider.scss"; + +export interface ThemeData { + isDark: boolean; +} + +export type OOMOLPrefersColorScheme = "auto" | "dark" | "light"; + +const prefersDark = window.matchMedia("(prefers-color-scheme: dark)"); + +export function useDarkMode( + prefersColorScheme?: OOMOLPrefersColorScheme, +): boolean { + const [darkMode, setDarkMode] = useState(() => + prefersColorScheme === "auto" && prefersDark + ? prefersDark.matches + : prefersColorScheme === "dark", + ); + + useEffect(() => { + if (prefersColorScheme === "auto" && prefersDark) { + setDarkMode(prefersDark.matches); + const handler = (event: MediaQueryListEvent): void => + setDarkMode(event.matches); + prefersDark.addEventListener("change", handler); + + return () => prefersDark.removeEventListener("change", handler); + } + else { + setDarkMode(prefersColorScheme === "dark"); + } + }, [prefersColorScheme]); + + return darkMode; +} + +export interface ThemeProviderProps { + children: React.ReactNode; + prefersColorScheme: OOMOLPrefersColorScheme; +} + +const ThemeContext = createContext({ isDark: false }); + +export const ThemeProvider = ({ + children, + prefersColorScheme, +}: PropsWithChildren): ReactElement => { + const darkMode = useDarkMode(prefersColorScheme); + + useIsomorphicLayoutEffect(() => { + document.body.classList.add("oomol-theme-root", "oomol-theme-light"); + document.body.classList.toggle("oomol-theme-dark", darkMode); + }, [darkMode]); + + const themeValue = useMemo(() => ({ isDark: darkMode }), [darkMode]); + + return ( + + {children} + + ); +}; + +export const useThemeData = (): ThemeData => { + const data = useContext(ThemeContext); + + if (!data) { + throw new Error("Must be used within a ThemeProvider"); + } + return data; +}; diff --git a/desktop/renderer/src/hooks/index.ts b/desktop/renderer/src/hooks/index.ts new file mode 100644 index 0000000..c012bf2 --- /dev/null +++ b/desktop/renderer/src/hooks/index.ts @@ -0,0 +1,2 @@ +export * from "./use-app-context"; +export * from "./use-isomorphic-layout-effect"; diff --git a/desktop/renderer/src/hooks/use-app-context.ts b/desktop/renderer/src/hooks/use-app-context.ts new file mode 100644 index 0000000..dedd580 --- /dev/null +++ b/desktop/renderer/src/hooks/use-app-context.ts @@ -0,0 +1,17 @@ +import { useContext } from "react"; +import { useVal } from "use-value-enhancer"; + +import { AppContextContext } from "~/components/AppContextProvider"; + +import type { AppContext } from "~/routes"; +import type { OSLiteral } from "~/routes/constants"; + +export const useAppContext = (): Readonly => { + const appContext = useContext(AppContextContext); + if (!appContext) { + throw new Error("AppContextProvider not found"); + } + return appContext; +}; + +export const useOS = (): OSLiteral => useVal(useAppContext().os$); diff --git a/desktop/renderer/src/hooks/use-isomorphic-layout-effect.ts b/desktop/renderer/src/hooks/use-isomorphic-layout-effect.ts new file mode 100644 index 0000000..e385c14 --- /dev/null +++ b/desktop/renderer/src/hooks/use-isomorphic-layout-effect.ts @@ -0,0 +1,10 @@ +import type { DependencyList, EffectCallback } from "react"; +import { useEffect, useLayoutEffect } from "react"; + +export const useIsomorphicLayoutEffect = ( + effect: EffectCallback, + deps?: DependencyList, +): void => { + const useIsoLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect; + return useIsoLayoutEffect(effect, deps); +}; diff --git a/desktop/renderer/src/index.tsx b/desktop/renderer/src/index.tsx index 83bbf8a..519a771 100644 --- a/desktop/renderer/src/index.tsx +++ b/desktop/renderer/src/index.tsx @@ -1,10 +1,20 @@ import type { AppContext } from "./routes"; import React from "react"; - import { createRoot } from "react-dom/client"; + +import { val } from "value-enhancer"; import { StudioHome } from "./App"; +import { OS } from "./routes/constants"; +import { SettingStore } from "./stores/setting.store"; +import "./style.css"; + +const os$ = val(OS.Windows); +const settingStore = new SettingStore(); -const appContext: AppContext = {}; +const appContext: AppContext = { + os$, + settingStore, +}; const root = createRoot(document.getElementById("root")!); root.render( diff --git a/desktop/renderer/src/routes/HomeRoot/HomeRoot.module.scss b/desktop/renderer/src/routes/HomeRoot/HomeRoot.module.scss new file mode 100644 index 0000000..453eaec --- /dev/null +++ b/desktop/renderer/src/routes/HomeRoot/HomeRoot.module.scss @@ -0,0 +1,97 @@ +@import "~/styles/variables.scss"; + +$header-height: 40px; + +.container { + width: 100%; + height: 100%; + display: flex; +} + +.sidebar { + flex-grow: 0; + flex-shrink: 0; + width: 160px; + height: 100%; + overflow: hidden; + display: flex; + flex-direction: column; + flex-wrap: nowrap; + box-sizing: border-box; + border-inline-end: 1px solid var(--hr-color); + background: var(--sidebar-background-color); +} + +.sidebar-mac { + padding-top: $header-height; + -webkit-app-region: drag; +} + +.sidebar-header { + height: $header-height; +} + +.sidebar-header-box { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: space-between; + .left { + margin-left: 15px; + } +} + +.sidebar-content { + flex: 1; + overflow-y: overlay; + + -webkit-app-region: no-drag; + z-index: 2; +} + +.sidebar-footer { + flex-grow: 0; + flex-shrink: 0; + margin-bottom: 24px; +} + +.sidebar-plan { + width: 100%; + display: flex; + align-items: center; + justify-content: center; +} + +.sidebar-plan-btn { + border-radius: 16px; +} + +.main { + flex: 1; + height: 100%; + overflow: hidden; + display: flex; + flex-direction: column; + flex-wrap: nowrap; +} + +.header { + height: $header-height; + box-sizing: border-box; + padding-right: $win-btn-group-width; + border-bottom: 1px solid var(--hr-color); + background: var(--header-background-color); + + -webkit-app-region: drag; +} + +.header-mac { + padding-right: 0; +} + +.content { + flex: 1; + overflow: hidden; + box-sizing: border-box; +} diff --git a/desktop/renderer/src/routes/HomeRoot/HomeRootTitleBar.tsx b/desktop/renderer/src/routes/HomeRoot/HomeRootTitleBar.tsx new file mode 100644 index 0000000..11c0323 --- /dev/null +++ b/desktop/renderer/src/routes/HomeRoot/HomeRootTitleBar.tsx @@ -0,0 +1,44 @@ +import type { FC } from "react"; +import type { Val } from "value-enhancer"; +import React from "react"; +import { useLocation } from "react-router-dom"; + +import { useVal } from "use-value-enhancer"; +import { HomeTitleBarLayout } from "~/components/HomeTitleBar"; +import { useIsomorphicLayoutEffect, useOS } from "~/hooks"; +import { OS } from "../constants"; + +export interface HomeRootTitleBarProps { + titleBar$: Val; +} + +const DefaultHomeTitleBar: FC = () => { + const os = useOS(); + return ( + + logo + + ) + } + /> + ); +}; + +export const HomeRootTitleBar = ({ + titleBar$, +}: HomeRootTitleBarProps) => { + const titleBar = useVal(titleBar$, true); + const location = useLocation(); + useIsomorphicLayoutEffect( + () => () => { + titleBar$.set(null); + }, + [location], + ); + return <>{titleBar || }; +}; + +interface DefaultHomeTitleBarProps {} diff --git a/desktop/renderer/src/routes/HomeRoot/SideNav.module.scss b/desktop/renderer/src/routes/HomeRoot/SideNav.module.scss new file mode 100644 index 0000000..2fd4faf --- /dev/null +++ b/desktop/renderer/src/routes/HomeRoot/SideNav.module.scss @@ -0,0 +1,13 @@ +.menu { + border: none !important; + background: transparent !important; + user-select: none; + + :global(.ant-menu-title-content > a) { + &, + &::before { + cursor: default; + -webkit-user-drag: none; + } + } +} diff --git a/desktop/renderer/src/routes/HomeRoot/SideNav.tsx b/desktop/renderer/src/routes/HomeRoot/SideNav.tsx new file mode 100644 index 0000000..d541c98 --- /dev/null +++ b/desktop/renderer/src/routes/HomeRoot/SideNav.tsx @@ -0,0 +1,63 @@ +import type { ReactNode } from "react"; +import { + NodeIndexOutlined, + SettingOutlined, +} from "@ant-design/icons"; + +import { Menu } from "antd"; + +import React, { useCallback, useMemo } from "react"; +import { Link, useLocation, useNavigate } from "react-router-dom"; +import { RoutePath } from "../constants"; + +import styles from "./SideNav.module.scss"; + +export const SideNav = () => { + const items = useMemo(() => { + const items: Array<{ + label: ReactNode; + key: string; + icon: ReactNode; + }> = [ + { + label: "Home", + key: RoutePath.projects, + icon: , + }, + { + label: "settings", + key: RoutePath.Settings, + icon: , + }, + ]; + + for (const item of items) { + item.label = {item.label}; + } + + return items; + }, []); + + const location = useLocation(); + + const selectedKeys = useMemo(() => { + const pathname = `/${location.pathname.split("/").slice(1, 3).join("/")}`; + return items.some(item => item.key === pathname) ? [pathname] : []; + }, [items, location.pathname]); + + const navigate = useNavigate(); + + const onSelect = useCallback( + (item: { key: string }) => navigate(item.key), + [navigate], + ); + + return ( + + ); +}; diff --git a/desktop/renderer/src/routes/HomeRoot/index.tsx b/desktop/renderer/src/routes/HomeRoot/index.tsx new file mode 100644 index 0000000..81b58aa --- /dev/null +++ b/desktop/renderer/src/routes/HomeRoot/index.tsx @@ -0,0 +1,55 @@ +import type { Val } from "value-enhancer"; +import React, { useState } from "react"; + +import { Outlet } from "react-router-dom"; + +import { val } from "value-enhancer"; +import { useOS } from "~/hooks"; +import type { RouteOutletContext } from "~/typings"; +import { OS } from "../constants"; +import styles from "./HomeRoot.module.scss"; +import { HomeRootTitleBar } from "./HomeRootTitleBar"; +import { SideNav } from "./SideNav"; + +export const HomeRoot = () => { + const os = useOS(); + const [titleBar$] = useState>(val); + const outletContext: RouteOutletContext = titleBar$.set; + + return ( +
+
+ {os !== OS.Mac && ( +
+
+
+ {/* TODO: add Logo */} +
+
+
+ )} +
+ +
+
+
+
+ +
+
+ +
+
+
+ ); +}; diff --git a/desktop/renderer/src/routes/Settings/index.module.scss b/desktop/renderer/src/routes/Settings/index.module.scss new file mode 100644 index 0000000..640b38f --- /dev/null +++ b/desktop/renderer/src/routes/Settings/index.module.scss @@ -0,0 +1,32 @@ +.container { + height: 100%; + width: 100%; + padding: 12px; +} + +.languageSetting { + display: flex; + flex-direction: column; +} + +.label { + height: 10px; +} + +.radioGroup { + padding-top: 22px; +} + +.modal { + :global(.ant-modal-body) { + display: flex; + align-items: center; + font-weight: 600; + } +} + +.icon { + color: var(--warning); + font-size: 24px; + margin-right: 6px; +} diff --git a/desktop/renderer/src/routes/Settings/index.tsx b/desktop/renderer/src/routes/Settings/index.tsx new file mode 100644 index 0000000..c667831 --- /dev/null +++ b/desktop/renderer/src/routes/Settings/index.tsx @@ -0,0 +1,55 @@ +import type { CheckboxChangeEvent } from "antd/es/checkbox"; +import React from "react"; + +import { useVal } from "use-value-enhancer"; +import { AppearancePicker } from "~/components/AppearancePicker"; +import type { OOMOLPrefersColorScheme } from "~/components/ThemeProvider"; +import { useAppContext } from "~/hooks"; + +import styles from "./index.module.scss"; + +export const Settings = () => { + // const t = useTranslate(); + // const language = useLang(); + const { settingStore } = useAppContext(); + + const prefersColorScheme = useVal(settingStore.prefersColorScheme$); + + // const [selectLanguage, setSelectLanguage] = useState(language as Lang); + + const changeAppearance = (event: CheckboxChangeEvent) => { + const prefersColorScheme: OOMOLPrefersColorScheme = event.target.value; + settingStore.updatePrefersColorScheme(prefersColorScheme); + }; + + return ( +
+ {/*

{t("setting.appearance")}

*/} +

Appearance

+ + {/*
*/} + {/* {t("setting.language")} */} + {/* + + {t("setting.chinese")} + + + English + + */} + {/*
*/} +
+ ); +}; diff --git a/desktop/renderer/src/routes/constants.ts b/desktop/renderer/src/routes/constants.ts index 4175e3f..88d0c49 100644 --- a/desktop/renderer/src/routes/constants.ts +++ b/desktop/renderer/src/routes/constants.ts @@ -1,4 +1,15 @@ export enum RoutePath { Root = "/", - Home = "/home", + HomeRoot = "/home", + projects = "/home/projects", + Settings = "/home/settings", } + +export enum OS { + Windows = "win", + Mac = "mac", + Linux = "linux", + Web = "web", +} + +export type OSLiteral = `${OS}`; diff --git a/desktop/renderer/src/routes/index.tsx b/desktop/renderer/src/routes/index.tsx index 0d9ce12..0e74d37 100644 --- a/desktop/renderer/src/routes/index.tsx +++ b/desktop/renderer/src/routes/index.tsx @@ -1,13 +1,22 @@ +import type { ReadonlyVal } from "value-enhancer"; +import type { OSLiteral } from "./constants"; + import React, { useMemo } from "react"; import { createHashRouter, Navigate, RouterProvider, } from "react-router-dom"; +import type { SettingStore } from "~/stores/setting.store"; import { RoutePath } from "./constants"; import { Home } from "./Home"; +import { HomeRoot } from "./HomeRoot"; +import { Settings } from "./Settings"; -export interface AppContext {}; +export interface AppContext { + os$: ReadonlyVal; + settingStore: SettingStore; +}; const createRouter = () => createHashRouter([ @@ -16,11 +25,25 @@ const createRouter = () => children: [ { path: RoutePath.Root, - element: , + element: , }, { - path: RoutePath.Home, - element: , + path: RoutePath.HomeRoot, + element: , + children: [ + { + path: RoutePath.HomeRoot, + element: , + }, + { + path: RoutePath.projects, + element: , + }, + { + path: RoutePath.Settings, + element: , + }, + ], }, ], }, diff --git a/desktop/renderer/src/stores/setting.store.ts b/desktop/renderer/src/stores/setting.store.ts new file mode 100644 index 0000000..7e4dbbf --- /dev/null +++ b/desktop/renderer/src/stores/setting.store.ts @@ -0,0 +1,27 @@ +import type { Val } from "value-enhancer"; +import { val } from "value-enhancer"; + +import type { OOMOLPrefersColorScheme } from "~/components/ThemeProvider"; + +export class SettingStore { + public localLanguage$: Val = val( + localStorage.getItem("language") ?? "en", + ); + + public prefersColorScheme$: Val = val( + (localStorage.getItem("prefersColorScheme") as OOMOLPrefersColorScheme) + ?? "dark", + ); + + public updateLocalLanguage(language: string): void { + this.localLanguage$.set(language); + localStorage.setItem("language", language); + } + + public updatePrefersColorScheme( + prefersColorScheme: OOMOLPrefersColorScheme, + ): void { + this.prefersColorScheme$.set(prefersColorScheme); + localStorage.setItem("prefersColorScheme", prefersColorScheme); + } +} diff --git a/desktop/renderer/src/style.css b/desktop/renderer/src/style.css new file mode 100644 index 0000000..0fed43a --- /dev/null +++ b/desktop/renderer/src/style.css @@ -0,0 +1,9 @@ +html, +body { + margin: 0; + padding: 0; + font-family: -apple-system, BlinkMacSystemFont, sans-serif; + color: var(--foreground); + background-color: var(--editor-background); + font-size: var(--base-font-size); +} diff --git a/desktop/renderer/src/styles/colors.scss b/desktop/renderer/src/styles/colors.scss new file mode 100644 index 0000000..ecae40a --- /dev/null +++ b/desktop/renderer/src/styles/colors.scss @@ -0,0 +1,71 @@ +$colors: ( + blue-0: #f4f8ff, + blue-1: #ebf2ff, + blue-2: #d6e5ff, + blue-3: #adccff, + blue-4: #84b3ff, + blue-5: #5b9aff, + blue-6: #3381ff, + blue-7: #2867cc, + blue-8: #1e4d99, + blue-9: #143366, + blue-10: #0a1933, + blue-11: #050d1a, + blue-12: #03060d, + + grey-0: #ecf0f7, + grey-1: #e5e8f0, + grey-2: #d5d9e0, + grey-3: #b7bbc1, + grey-4: #999ca3, + grey-5: #7b7e84, + grey-6: #5d6066, + grey-7: #4b4d54, + grey-8: #383b42, + grey-9: #272a30, + grey-10: #14181e, + grey-11: #070a11, + grey-12: #03060d, + + green-0: #f5faf2, + green-1: #ecf6e6, + green-2: #d9eecc, + green-3: #b4de99, + green-4: #8ecd66, + green-5: #69bd33, + green-6: #44ad00, + green-7: #368b00, + green-8: #296800, + green-9: #1b4500, + green-10: #0d2200, + green-11: #071100, + green-12: #030900, + + yellow-0: #fdf9f2, + yellow-1: #fcf4e6, + yellow-2: #faeacc, + yellow-3: #f5d599, + yellow-4: #f1c166, + yellow-5: #ecac33, + yellow-6: #e89800, + yellow-7: #ba7a00, + yellow-8: #8b5b00, + yellow-9: #5c3c00, + yellow-10: #2e1e00, + yellow-11: #170f00, + yellow-12: #0c0800, + + red-0: #fcf3f2, + red-1: #fae9e6, + red-2: #f6d2cc, + red-3: #eda599, + red-4: #e47866, + red-5: #db4b33, + red-6: #d21f00, + red-7: #a81800, + red-8: #7e1300, + red-9: #540c00, + red-10: #2a0600, + red-11: #150300, + red-12: #0a0200, +); diff --git a/desktop/renderer/src/styles/theme.scss b/desktop/renderer/src/styles/theme.scss new file mode 100644 index 0000000..0e08561 --- /dev/null +++ b/desktop/renderer/src/styles/theme.scss @@ -0,0 +1,93 @@ +@use "sass:map"; +@import "./colors.scss"; + +@mixin theme-root { + @extend .oomol-colors-root; + --brand: var(--blue-6); + + --primary: var(--blue-6); + --primary-strong: var(--blue-7); + --primary-stronger: var(--blue-8); + --primary-weak: var(--blue-5); + --primary-weaker: var(--blue-2); + + --danger: var(--red-6); + --danger-strong: var(--red-7); + --danger-stronger: var(--red-8); + --danger-weak: var(--red-5); + --danger-weaker: var(--red-2); + + --success: var(--green-6); + --success-strong: var(--green-7); + --success-stronger: var(--green-8); + --success-weak: var(--green-5); + --success-weaker: var(--green-2); + + --warning: var(--yellow-6); + --warning-strong: var(--yellow-7); + --warning-stronger: var(--yellow-8); + --warning-weak: var(--yellow-5); + --warning-weaker: var(--yellow-2); + + --text: var(--grey-6); + --text-strong: var(--grey-8); + --text-stronger: var(--grey-12); + --text-weak: var(--grey-5); + --text-weaker: var(--grey-3); + + --link: var(--blue-6); + --link-hover: var(--blue-5); + --link-active: var(--blue-7); + --link-focus: var(--blue-5); + --link-visited: var(--blue-3); + + width: 100%; + height: 100%; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, + "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", + "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + color: var(--text-color); + background: var(--background-color); +} + +@mixin theme-light { + color-scheme: light; + + --text-color: rgba(0, 0, 0, 0.88); + --background-color: #fff; + --header-background-color: #fff; + --sidebar-background-color: #f6f6f7; + --hr-color: rgba(0, 0, 0, 0.12); + --inner-bg-color: #fafafa; + --notification-right-border: #f3f3f3; + --segmented-bg-color: #ecf0f7; +} + +@mixin theme-dark { + color-scheme: dark; + + --text-color: rgba(255, 255, 255, 0.85); + --background-color: #000000; + --header-background-color: #000000; + --sidebar-background-color: #1a1a1a; + --hr-color: #363636; + + --inner-bg-color: #202020; + --notification-right-border: #3b3b3b; + --segmented-bg-color: #383838; +} + +@mixin genColors($name, $counter: 0) { + @while $counter <= 12 { + --#{$name}-#{$counter}: #{map-get($colors, $name + "-" + $counter)}; + $counter: $counter + 1; + } +} + +.oomol-colors-root { + @include genColors("blue"); + @include genColors("grey"); + @include genColors("green"); + @include genColors("yellow"); + @include genColors("red"); +} diff --git a/desktop/renderer/src/styles/variables.scss b/desktop/renderer/src/styles/variables.scss new file mode 100644 index 0000000..3de2a1d --- /dev/null +++ b/desktop/renderer/src/styles/variables.scss @@ -0,0 +1,5 @@ +// Windows Button is the system button on the top right of Windows and Linux +$win-btn-width: 35px; +$win-btn-height: 35px; +$win-btn-group-width: $win-btn-width * 3; +$win-btn-group-height: $win-btn-height; diff --git a/desktop/renderer/src/typings.ts b/desktop/renderer/src/typings.ts new file mode 100644 index 0000000..998fd94 --- /dev/null +++ b/desktop/renderer/src/typings.ts @@ -0,0 +1,4 @@ +/** + * Currently only a `setTitleBar` setState function is exposed. Expand according to needs. + */ +export type RouteOutletContext = (titleBar: React.ReactNode) => void; diff --git a/desktop/renderer/src/vite-env.d.ts b/desktop/renderer/src/vite-env.d.ts new file mode 100644 index 0000000..7a07d1f --- /dev/null +++ b/desktop/renderer/src/vite-env.d.ts @@ -0,0 +1,3 @@ +/// + +declare const __DEV_SERVER_URL__: string; diff --git a/desktop/renderer/tsconfig.json b/desktop/renderer/tsconfig.json index 2f07ee4..66c7137 100644 --- a/desktop/renderer/tsconfig.json +++ b/desktop/renderer/tsconfig.json @@ -1,5 +1,10 @@ { "extends": "../../tsconfig.json", + "compilerOptions": { + "paths": { + "~/*": ["./src/*"], + }, + }, "include": [ "src/**/*", "typings/**/*", diff --git a/desktop/renderer/vite.config.ts b/desktop/renderer/vite.config.ts index 14fa121..276def8 100644 --- a/desktop/renderer/vite.config.ts +++ b/desktop/renderer/vite.config.ts @@ -1,5 +1,6 @@ import crypto from "node:crypto"; import path from "node:path"; +import { fileURLToPath } from "node:url"; import { defineConfig } from "vite"; export default defineConfig(({ mode }) => { @@ -12,6 +13,11 @@ export default defineConfig(({ mode }) => { css: { modules: { generateScopedName: createGenerateScopedName() }, }, + resolve: { + alias: { + "~": fileURLToPath(new URL("./src", import.meta.url)), + }, + }, server: { port: 7083, fs: { diff --git a/package.json b/package.json index a102593..338c91a 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,9 @@ "@eslint-react/eslint-plugin": "1.15.0", "eslint": "9.13.0", "eslint-plugin-react-hooks": "5.0.0", - "eslint-plugin-react-refresh": "0.4.14" + "eslint-plugin-react-refresh": "0.4.14", + "value-enhancer": "^5.4.2", + "use-value-enhancer": "^5.0.6" }, "patchedDependencies": { "slate-react@0.107.1": "patches/slate-react@0.107.1.patch" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5f8e756..329457b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,6 +10,8 @@ overrides: eslint: 9.13.0 eslint-plugin-react-hooks: 5.0.0 eslint-plugin-react-refresh: 0.4.14 + value-enhancer: ^5.4.2 + use-value-enhancer: ^5.0.6 patchedDependencies: slate-react@0.107.1: @@ -44,6 +46,12 @@ importers: eslint: specifier: 9.13.0 version: 9.13.0 + use-value-enhancer: + specifier: ^5.0.6 + version: 5.0.6(react@18.3.1)(value-enhancer@5.5.0) + value-enhancer: + specifier: ^5.4.2 + version: 5.5.0 vite: specifier: ^5.4.2 version: 5.4.10(@types/node@20.17.0)(less@4.2.0)(sass@1.80.4) @@ -3259,7 +3267,7 @@ packages: resolution: {integrity: sha512-iYHGcmrmJ63wD5gbPKhN23c0s3owPE8Hv+20NVpUXsL+WfsikLBMS3Ug1OB4B47JfTEHFZtXVXx6Z2245gvvDQ==} peerDependencies: react: '>=16' - value-enhancer: '5' + value-enhancer: ^5.4.2 util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}