diff --git a/apps/frontend/src/MancalaApp.tsx b/apps/frontend/src/MancalaApp.tsx index b381e91..bf2a1f8 100644 --- a/apps/frontend/src/MancalaApp.tsx +++ b/apps/frontend/src/MancalaApp.tsx @@ -11,7 +11,6 @@ import Home from './routes/Home'; import { initContext } from './context/context'; import { RTMTWS, ConnectionState, AuthUser } from '@mancala/core'; -import { getColorByBrightness } from "@mancala/core"; import { Theme } from '@mancala/core'; import LobyPage from './routes/LobyPage'; import PrivacyPage from './routes/PrivacyPage'; @@ -19,6 +18,8 @@ import swal from 'sweetalert'; import Util from './util/Util'; import { server } from './const/config'; import { useTranslation } from 'react-i18next'; +import { applyThemeClass } from './theme/themes'; +import './styles/app.css'; const context = initContext(); @@ -42,7 +43,10 @@ const MancalaApp: FunctionComponent = () => { const onConnectionChange = (_connectionState: ConnectionState) => setConnetionState(_connectionState); - const onThemeChange = (theme: Theme) => setTheme(theme); + const onThemeChange = (theme: Theme) => { + applyThemeClass(theme); + setTheme(theme); + }; React.useEffect(() => { //@ts-ignore @@ -99,6 +103,7 @@ const MancalaApp: FunctionComponent = () => { } React.useEffect(() => { + applyThemeClass(context.themeManager.theme); startSession().catch((error) => { swal(t("Error") + "!", t("ErrorWhenRetrievingInformation"), "error"); console.error(error); @@ -112,11 +117,6 @@ const MancalaApp: FunctionComponent = () => { }; }, []); - const textColorOnBoard = getColorByBrightness( - context.themeManager.theme.boardColor, - context.themeManager.theme.textColor, - context.themeManager.theme.textLightColor - ); return ( <> {hasSession && @@ -133,8 +133,8 @@ const MancalaApp: FunctionComponent = () => { } - - {Util.getTextByConnectionState(context, connectionState, t)} + + {Util.getTextByConnectionState(context, connectionState, t)} ); diff --git a/apps/frontend/src/components/FloatingPanel.tsx b/apps/frontend/src/components/FloatingPanel.tsx index 564ec06..779a95d 100644 --- a/apps/frontend/src/components/FloatingPanel.tsx +++ b/apps/frontend/src/components/FloatingPanel.tsx @@ -1,32 +1,38 @@ import * as React from "react"; import { FunctionComponent } from "react"; -import { Context } from "../context/context"; -const FloatingPanel: FunctionComponent<{ - context: Context; - color: string; - visible: boolean; -}> = (props) => { - if(props.visible === false) return <> +// A status pill in the bottom left corner, e.g. while connecting. +const FloatingPanel: FunctionComponent<{ visible: boolean }> = (props) => { + if (props.visible === false) return <> return ( -
+
+ + {props.children} - {props.children}
) }; -export default FloatingPanel; \ No newline at end of file +export default FloatingPanel; diff --git a/apps/frontend/src/components/Icons.tsx b/apps/frontend/src/components/Icons.tsx new file mode 100644 index 0000000..49fadab --- /dev/null +++ b/apps/frontend/src/components/Icons.tsx @@ -0,0 +1,69 @@ +import * as React from "react"; + +// Inline stroke icons in the current text colour. All are decorative; buttons carry their own labels. + +type IconProps = { size?: number, className?: string }; + +const stroke = (size: number, width: number, className?: string) => ({ + width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: width, + strokeLinecap: "round" as const, strokeLinejoin: "round" as const, className, "aria-hidden": true, +}); + +export const LogoIcon = ({ size = 34 }: IconProps) => ( + +); + +export const PlusIcon = ({ size = 20 }: IconProps) => ( + +); + +export const ProfileIcon = ({ size = 20 }: IconProps) => ( + +); + +export const LeaveIcon = ({ size = 20 }: IconProps) => ( + +); + +export const ChevronIcon = ({ size = 20, className }: IconProps) => ( + +); + +export const CloseIcon = ({ size = 20 }: IconProps) => ( + +); + +export const ReplayIcon = ({ size = 18 }: IconProps) => ( + +); + +export const FirstIcon = ({ size = 20 }: IconProps) => ( + +); + +export const PreviousIcon = ({ size = 20 }: IconProps) => ( + +); + +export const NextIcon = ({ size = 20 }: IconProps) => ( + +); + +export const LastIcon = ({ size = 20 }: IconProps) => ( + +); + +export const PlayIcon = ({ size = 22 }: IconProps) => ( + +); + +export const PauseIcon = ({ size = 22 }: IconProps) => ( + +); diff --git a/apps/frontend/src/components/LoadingComponent.tsx b/apps/frontend/src/components/LoadingComponent.tsx index ea0e15c..2538447 100644 --- a/apps/frontend/src/components/LoadingComponent.tsx +++ b/apps/frontend/src/components/LoadingComponent.tsx @@ -1,32 +1,33 @@ import * as React from 'react'; import { FunctionComponent } from 'react'; -import { Context } from '../context/context'; import { LoadingState } from "@mancala/core"; -import { getColorByBrightness } from "@mancala/core"; -import CircularPanel from './CircularPanel'; import { useTranslation } from 'react-i18next'; -const LoadingComponent: FunctionComponent<{ context: Context, loadingState: LoadingState }> = ({ context, loadingState }) => { +const LoadingComponent: FunctionComponent<{ loadingState: LoadingState }> = ({ loadingState }) => { const { t } = useTranslation(); - if (loadingState.isUnset() || loadingState.isLoaded()) { - return <> - } - if (loadingState.isLoading() || loadingState.isError()) { - const textColorOnBoard = getColorByBrightness( - context.themeManager.theme.boardColor, - context.themeManager.theme.textColor, - context.themeManager.theme.textLightColor - ); - const text = loadingState.isLoading() ? t("Loading") +"..." : loadingState.errorMessage; - return ( - -

{`${text}`}

-
- ); - } - return <> + if (!loadingState.isLoading() && !loadingState.isError()) return <>; + const text = loadingState.isLoading() ? t("Loading") + "..." : loadingState.errorMessage; + return ( +
+ {loadingState.isLoading() && } + {text} + +
+ ); } -export default LoadingComponent; \ No newline at end of file +export default LoadingComponent; diff --git a/apps/frontend/src/components/PageContainer.tsx b/apps/frontend/src/components/PageContainer.tsx index 8740ee2..6db896a 100644 --- a/apps/frontend/src/components/PageContainer.tsx +++ b/apps/frontend/src/components/PageContainer.tsx @@ -1,25 +1,7 @@ import * as React from "react"; import { FunctionComponent } from "react"; -import { Theme } from "@mancala/core"; -const PageContainer: FunctionComponent<{ theme: Theme }> = (props) => { - return ( -
- - {props.children} -
- ); -} +// A page: the header, then the page's content. +const PageContainer: FunctionComponent = ({ children }) =>
{children}
; -export default PageContainer; \ No newline at end of file +export default PageContainer; diff --git a/apps/frontend/src/components/Row.tsx b/apps/frontend/src/components/Row.tsx deleted file mode 100644 index bc0db42..0000000 --- a/apps/frontend/src/components/Row.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import * as React from 'react'; -import { FunctionComponent } from 'react'; - -const Row: FunctionComponent = ({children}) => { - return ( -
- {children} - -
- ); -} - -export default Row; \ No newline at end of file diff --git a/apps/frontend/src/components/headerbar/AccountMenu.tsx b/apps/frontend/src/components/headerbar/AccountMenu.tsx index dc20841..f971cf6 100644 --- a/apps/frontend/src/components/headerbar/AccountMenu.tsx +++ b/apps/frontend/src/components/headerbar/AccountMenu.tsx @@ -4,46 +4,30 @@ import { FunctionComponent } from "react"; import { AuthUser, getPlayerDisplayName } from "@mancala/core"; import { useTranslation } from "react-i18next"; import { Context } from "../../context/context"; +import { ProfileIcon } from "../Icons"; import "@szhsin/react-menu/dist/index.css"; import "@szhsin/react-menu/dist/transitions/slide.css" // Guests can log in or sign up; logged in users can log out. -const AccountMenu: FunctionComponent<{ context: Context, textColor: string, authUser?: AuthUser }> = (props) => { - const { context, textColor, authUser } = props; +const AccountMenu: FunctionComponent<{ context: Context, authUser: AuthUser }> = ({ context, authUser }) => { const { t } = useTranslation(); - if (!authUser) return <>; - const authService = context.authService; - const menuButton = - account_circle - ; - const itemStyle = { color: textColor }; + const menuButton = ( + + ); const menuItems = authUser.isGuest ? [ - authService.login()}>{t("LogIn")}, - authService.signUp()}>{t("SignUp")}, + authService.login()}>{t("LogIn")}, + authService.signUp()}>{t("SignUp")}, ] : [ - authService.logout()}>{t("LogOut")}, + authService.logout()}>{t("LogOut")}, ]; return ( -
- - {getPlayerDisplayName(authUser, t)} - {menuItems} - - -
+ + {getPlayerDisplayName(authUser, t)} + {menuItems} + ); } diff --git a/apps/frontend/src/components/headerbar/AppHeader.tsx b/apps/frontend/src/components/headerbar/AppHeader.tsx new file mode 100644 index 0000000..236e159 --- /dev/null +++ b/apps/frontend/src/components/headerbar/AppHeader.tsx @@ -0,0 +1,28 @@ +import * as React from "react"; +import { FunctionComponent } from "react"; +import { Link } from "react-router-dom"; +import { AuthUser } from "@mancala/core"; +import { useTranslation } from "react-i18next"; +import { Context } from "../../context/context"; +import { LogoIcon } from "../Icons"; +import AccountMenu from "./AccountMenu"; +import ThemeSwatches from "./ThemeSwatches"; + +// The header of every page: logo and name, theme swatches, the account menu when a user is given, and the page's +// main action (children). +const AppHeader: FunctionComponent<{ context: Context, authUser?: AuthUser }> = ({ context, authUser, children }) => { + const { t } = useTranslation(); + return ( +
+ + + {t("Mancala")} + + + {authUser && } + {children} +
+ ); +}; + +export default AppHeader; diff --git a/apps/frontend/src/components/headerbar/HeaderBar.tsx b/apps/frontend/src/components/headerbar/HeaderBar.tsx deleted file mode 100644 index 683f8c2..0000000 --- a/apps/frontend/src/components/headerbar/HeaderBar.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import * as React from "react"; -import { FunctionComponent } from "react";; - -const HeaderBar: FunctionComponent<{ color?: string }> = ({children, color }) => { - return ( -
- {children} - -
) -} -export default HeaderBar; diff --git a/apps/frontend/src/components/headerbar/HeaderbarIcon.tsx b/apps/frontend/src/components/headerbar/HeaderbarIcon.tsx deleted file mode 100644 index b012df0..0000000 --- a/apps/frontend/src/components/headerbar/HeaderbarIcon.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import * as React from "react"; -import { FunctionComponent } from "react"; -//@ts-ignore -import MancalaIcon from "jsx:../../mancala.svg"; - -const HeaderbarIcon: FunctionComponent = () => { - return ( -
- - -
- ); -} - -export default HeaderbarIcon; \ No newline at end of file diff --git a/apps/frontend/src/components/headerbar/HeaderbarTitle.tsx b/apps/frontend/src/components/headerbar/HeaderbarTitle.tsx deleted file mode 100644 index 73e2bb0..0000000 --- a/apps/frontend/src/components/headerbar/HeaderbarTitle.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import * as React from 'react'; -import { FunctionComponent } from 'react'; - -const HeaderbarTitle: FunctionComponent<{ title: string, color: string }> = ({ title, color }) => { - return ( -

- {title} - -

- ); -} - -export default HeaderbarTitle; \ No newline at end of file diff --git a/apps/frontend/src/components/headerbar/ThemeSwatches.tsx b/apps/frontend/src/components/headerbar/ThemeSwatches.tsx new file mode 100644 index 0000000..add43d9 --- /dev/null +++ b/apps/frontend/src/components/headerbar/ThemeSwatches.tsx @@ -0,0 +1,26 @@ +import * as React from "react"; +import { FunctionComponent } from "react"; +import { useTranslation } from "react-i18next"; +import { Context } from "../../context/context"; +import { getWebTheme, webThemes } from "../../theme/themes"; + +const ThemeSwatches: FunctionComponent<{ context: Context }> = ({ context }) => { + const { t } = useTranslation(); + const current = getWebTheme(context.themeManager.theme); + return ( +
+ {webThemes.map((theme) => ( + + ))} +
+ ); +}; + +export default ThemeSwatches; diff --git a/apps/frontend/src/components/headerbar/ThemeSwitchMenu.tsx b/apps/frontend/src/components/headerbar/ThemeSwitchMenu.tsx deleted file mode 100644 index 6e45818..0000000 --- a/apps/frontend/src/components/headerbar/ThemeSwitchMenu.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { Menu, MenuItem } from "@szhsin/react-menu"; -import * as React from "react"; -import { FunctionComponent } from "react"; -import { Context } from "../../context/context"; -import "@szhsin/react-menu/dist/index.css"; -import "@szhsin/react-menu/dist/transitions/slide.css" - -const ThemeSwitchMenu: FunctionComponent<{ context: Context, textColor: string }> = (props) => { - const { context, textColor } = props; - const menuButton = - light_mode - ; - const menuItems = context.themeManager.themes.map((theme, index) => { - return ( - { - const htmlElement: HTMLElement = event.target as HTMLElement; - if (htmlElement.localName === "li") htmlElement.style.background = "transparent"; - }} - //@ts-ignore - onMouseOut={(event) => { - const htmlElement: HTMLElement = event.target as HTMLElement; - if (htmlElement.localName === "li") htmlElement.style.background = "transparent"; - }} - onClick={() => (context.themeManager.theme = theme)}> -
- {theme.name} - - - ); - }) - return ( -
- - {menuItems} - - -
- ); -} - -export default ThemeSwitchMenu; \ No newline at end of file diff --git a/apps/frontend/src/components/history/ReplayView.tsx b/apps/frontend/src/components/history/ReplayView.tsx index f3a2f4f..0a03491 100644 --- a/apps/frontend/src/components/history/ReplayView.tsx +++ b/apps/frontend/src/components/history/ReplayView.tsx @@ -24,13 +24,10 @@ import { formatDate, formatDuration, getResultText, getScoreText, getViewerSeat import { getMoveText } from "../../util/MoveText"; import BoardView from "../board/BoardView"; import Button from "../Button"; -import HeaderBar from "../headerbar/HeaderBar"; -import HeaderbarIcon from "../headerbar/HeaderbarIcon"; -import HeaderbarTitle from "../headerbar/HeaderbarTitle"; -import ThemeSwitchMenu from "../headerbar/ThemeSwitchMenu"; import PageContainer from "../PageContainer"; +import AppHeader from "../headerbar/AppHeader"; +import { PlusIcon } from "../Icons"; import GameClockView from "../GameClockView"; -import Row from "../Row"; import MoveList from "./MoveList"; const SPEEDS = [1, 2, 4]; @@ -166,19 +163,12 @@ const ReplayView: FunctionComponent<{ context: Context, theme: Theme, record: Ga ); return ( - - - - - - - - - - - +
diff --git a/apps/frontend/src/context/context.tsx b/apps/frontend/src/context/context.tsx index b848039..e881751 100644 --- a/apps/frontend/src/context/context.tsx +++ b/apps/frontend/src/context/context.tsx @@ -5,6 +5,7 @@ import { GameStore, GameStoreImpl } from "../store/GameStore"; import { AuthService } from "../auth/AuthService"; import { LocalStorage } from "../storage"; import { Storage } from '@mancala/core' +import { webThemes } from "../theme/themes"; export type Context = { rtmt: RTMT; @@ -20,7 +21,7 @@ export const initContext = (): Context => { const authService = new AuthService({ keycloakUrl: keycloak.url, realm: keycloak.realm, clientId: keycloak.clientId }); const gameStore = new GameStoreImpl({ httpService }); const storage = new LocalStorage(); - const themeManager = new ThemeManager(storage); + const themeManager = new ThemeManager(storage, webThemes); return { rtmt: rtmt, authService, diff --git a/apps/frontend/src/index.html b/apps/frontend/src/index.html index 3e12ef0..156bef2 100644 --- a/apps/frontend/src/index.html +++ b/apps/frontend/src/index.html @@ -1,31 +1,18 @@ - + Mancala + + + - -
+ +
- - - - - \ No newline at end of file + diff --git a/apps/frontend/src/routes/GamePage.tsx b/apps/frontend/src/routes/GamePage.tsx index e0e776e..44840ee 100644 --- a/apps/frontend/src/routes/GamePage.tsx +++ b/apps/frontend/src/routes/GamePage.tsx @@ -2,20 +2,15 @@ import { MancalaGame, Pit } from 'mancala.js'; import * as React from 'react'; import { FunctionComponent, useState } from 'react'; import { useNavigate, useParams } from 'react-router'; -import { Link } from 'react-router-dom'; import { v4 } from 'uuid'; import { PitAnimator } from '@mancala/core'; import BoardToolbar from '../components/board/BoardToolbar'; import BoardView from '../components/board/BoardView'; -import Button from '../components/Button'; -import HeaderBar from '../components/headerbar/HeaderBar'; -import HeaderbarIcon from '../components/headerbar/HeaderbarIcon'; -import HeaderbarTitle from '../components/headerbar/HeaderbarTitle'; -import ThemeSwitchMenu from '../components/headerbar/ThemeSwitchMenu'; import InfoPanel from '../components/InfoPanel'; import LoadingComponent from '../components/LoadingComponent'; import PageContainer from '../components/PageContainer'; -import Row from '../components/Row'; +import AppHeader from '../components/headerbar/AppHeader'; +import { LeaveIcon, PlusIcon } from '../components/Icons'; import UserStatus from '../components/UserStatus'; import { GameRecord, channel_on_game_update, channel_on_game_crashed, channel_on_game_end, channel_on_game_user_leave, channel_on_user_connection_change, channel_leave_game, channel_game_move, channel_listen_game_events, channel_unlisten_game_events, FinishedGameSummary, GameEndMessage, getBoardAfterMove, getSeat, PlayedMove } from '@mancala/core'; import { Context } from '../context/context'; @@ -341,7 +336,7 @@ const GamePage: FunctionComponent<{ } return ( - + {renderHeaderBar()} {renderMobileBoardToolbar()} {buildBoardTopToolbar()} @@ -371,30 +366,21 @@ const GamePage: FunctionComponent<{ collapsedByDefault={isMobile} /> )}
- +
); function renderHeaderBar() { - return - - - - - - - - - - - + : } + ; } function renderMobileBoardToolbar() { diff --git a/apps/frontend/src/routes/Home.tsx b/apps/frontend/src/routes/Home.tsx index d3959cf..b9bc59f 100644 --- a/apps/frontend/src/routes/Home.tsx +++ b/apps/frontend/src/routes/Home.tsx @@ -1,20 +1,15 @@ import * as React from "react"; import { FunctionComponent, useEffect, useState } from "react"; -import { getColorByBrightness, LiveGameSummary } from "@mancala/core"; +import { LiveGameSummary } from "@mancala/core"; import { AuthUser, Theme } from "@mancala/core"; -import HeaderBar from "../components/headerbar/HeaderBar"; +import AppHeader from "../components/headerbar/AppHeader"; import PageContainer from "../components/PageContainer"; -import Row from "../components/Row"; -import HeaderbarIcon from "../components/headerbar/HeaderbarIcon"; -import HeaderbarTitle from "../components/headerbar/HeaderbarTitle"; -import ThemeSwitchMenu from "../components/headerbar/ThemeSwitchMenu"; -import AccountMenu from "../components/headerbar/AccountMenu"; +import { PlusIcon } from "../components/Icons"; import ActiveGameBanner from "../components/home/ActiveGameBanner"; import LiveGamesSection from "../components/home/LiveGamesSection"; import RecentGamesSection from "../components/home/RecentGamesSection"; -import Button from "../components/Button"; import { Context } from "../context/context"; -import { Link, useNavigate } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; import Util from "../util/Util"; import { useTranslation } from "react-i18next"; @@ -46,32 +41,13 @@ const Home: FunctionComponent<{ navigate("/loby") }; - const textColorOnAppBar = getColorByBrightness( - context.themeManager.theme.appBarBgColor, - context.themeManager.theme.textColor, - context.themeManager.theme.textLightColor - ); return ( - - - - - - - - - - - - - - + {activeGame && authUser && ( = ({ context, userKey, theme }) => { +}> = ({ context }) => { let navigate = useNavigate(); @@ -52,58 +44,60 @@ const LobyPage: FunctionComponent<{ context.rtmt.removeMessageListener(channel_on_new_game_rejected, onNewGameRejected); } }, []); - - const textColorOnAppBar = getColorByBrightness( - context.themeManager.theme.appBarBgColor, - context.themeManager.theme.textColor, - context.themeManager.theme.textLightColor - ); - const textColorOnBoard = getColorByBrightness( - context.themeManager.theme.boardColor, - context.themeManager.theme.textColor, - context.themeManager.theme.textLightColor - ); - return ( - - - - - - - - - - - - - - -
-
- -

- {activeGameId ? t("GameInProgress") : `${t("SearchingOpponent")} ${t("PleaseWait")}...`} -

-
- {activeGameId &&
-
+ return ( + + +
+
+ {!activeGameId && } +

{activeGameId ? t("GameInProgress") : t("SearchingOpponent")}

+ {!activeGameId &&

{`${t("PleaseWait")}...`}

} +
+ {activeGameId && ( + + )} + +
+
+
+
); } -export default LobyPage; \ No newline at end of file +export default LobyPage; diff --git a/apps/frontend/src/routes/PrivacyPage.tsx b/apps/frontend/src/routes/PrivacyPage.tsx index a5096e0..177dd1a 100644 --- a/apps/frontend/src/routes/PrivacyPage.tsx +++ b/apps/frontend/src/routes/PrivacyPage.tsx @@ -1,56 +1,19 @@ import * as React from 'react'; -import { FunctionComponent, useEffect } from 'react'; -import { Link, useNavigate } from 'react-router-dom'; -import HeaderBar from '../components/headerbar/HeaderBar'; -import HeaderbarIcon from '../components/headerbar/HeaderbarIcon'; -import HeaderbarTitle from '../components/headerbar/HeaderbarTitle'; -import ThemeSwitchMenu from '../components/headerbar/ThemeSwitchMenu'; +import { FunctionComponent } from 'react'; +import AppHeader from '../components/headerbar/AppHeader'; import PageContainer from '../components/PageContainer'; -import Row from '../components/Row'; import { Context } from '../context/context'; import { Theme } from '@mancala/core'; -import { getColorByBrightness } from "@mancala/core"; -import { useTranslation } from 'react-i18next'; const PrivacyPage: FunctionComponent<{ context: Context, userKey?: string, theme: Theme -}> = ({ context, userKey, theme }) => { - - const { t } = useTranslation(); - - const textColorOnAppBar = getColorByBrightness( - context.themeManager.theme.appBarBgColor, - context.themeManager.theme.textColor, - context.themeManager.theme.textLightColor - ); - const textColorOnBoard = getColorByBrightness( - context.themeManager.theme.background, - "#000", - "#aaa" - ); +}> = ({ context }) => { return ( - - - - - - - - - - - - - - -
- + + +

Privacy Policy

The Mancala team and volunteers built the Mancala app as a open source app. This SERVICE is provided by mancala team and volunteers at no cost and is intended for use as is.

@@ -82,9 +45,22 @@ const PrivacyPage: FunctionComponent<{

Contact Us

If you have any questions or suggestions about our Privacy Policy, do not hesitate to contact us.

-
+ +
); } -export default PrivacyPage; \ No newline at end of file +export default PrivacyPage; diff --git a/apps/frontend/src/styles/app.css b/apps/frontend/src/styles/app.css new file mode 100644 index 0000000..99c6653 --- /dev/null +++ b/apps/frontend/src/styles/app.css @@ -0,0 +1,244 @@ +/* Themes, base styles and the building blocks shared by the pages. The theme class is set on . */ + +.t-ceviz { + --bg: #1f1712; --surface: #2a2019; --surface2: #382a20; --line: rgba(246, 234, 216, .10); + --ink: #f6ead8; --muted: #c4ad93; --accent: #f2b84b; --onAccent: #2a1a0a; + --accentSoft: rgba(242, 184, 75, .45); --accentGlow: rgba(242, 184, 75, .35); --lastRing: rgba(246, 234, 216, .55); + --chip: #1a110b; --chipInk: #f6ead8; --rim: #8a5d3b; --pit: #2c1d13; --live: #ff8a73; + --miniBg: #5b3d28; --miniPit: #2c1d13; + --boardBg: repeating-linear-gradient(94deg, rgba(255, 255, 255, .025) 0 3px, rgba(0, 0, 0, .04) 3px 9px, rgba(255, 255, 255, .015) 9px 14px), linear-gradient(180deg, #6e4a30, #573822); + --boardShadow: inset 0 1px 0 rgba(255, 255, 255, .12), inset 0 -3px 0 rgba(0, 0, 0, .25), 0 26px 50px rgba(0, 0, 0, .45), 0 0 0 1px #3a2516; + --pitShadow: inset 0 8px 16px rgba(0, 0, 0, .6), inset 0 -2px 0 rgba(255, 255, 255, .07), 0 1px 0 rgba(255, 255, 255, .08); + --stoneShadow: 0 3px 4px rgba(0, 0, 0, .45); + color-scheme: dark; +} + +.t-gece { + --bg: #0c1220; --surface: #141c30; --surface2: #1d2744; --line: rgba(238, 241, 248, .09); + --ink: #eef1f8; --muted: #9aa6c2; --accent: #e2b36a; --onAccent: #241707; + --accentSoft: rgba(226, 179, 106, .45); --accentGlow: rgba(226, 179, 106, .35); --lastRing: rgba(238, 241, 248, .5); + --chip: #0a0f1c; --chipInk: #eef1f8; --rim: #b8925a; --pit: #0b1020; --live: #ff8a7a; + --miniBg: #1d2a4d; --miniPit: #0b1020; + --boardBg: linear-gradient(180deg, #1d2a4d, #141d38); + --boardShadow: inset 0 0 0 2px #b8925a, inset 0 0 0 7px #18223f, inset 0 0 0 8px rgba(184, 146, 90, .35), 0 26px 50px rgba(0, 0, 0, .5); + --pitShadow: inset 0 8px 18px rgba(0, 0, 0, .7), inset 0 0 0 1px rgba(184, 146, 90, .28), 0 1px 0 rgba(255, 255, 255, .06); + --stoneShadow: 0 3px 5px rgba(0, 0, 0, .55); + color-scheme: dark; +} + +.t-yesim { + --bg: #e9ebe3; --surface: #f7f8f3; --surface2: #dde0d3; --line: rgba(29, 39, 33, .12); + --ink: #1d2721; --muted: #4f5d55; --accent: #2a7650; --onAccent: #ffffff; + --accentSoft: rgba(42, 118, 80, .4); --accentGlow: rgba(42, 118, 80, .28); --lastRing: rgba(29, 39, 33, .5); + --chip: #1d2721; --chipInk: #ffffff; --rim: #e9ebe3; --pit: #c3c7b6; --live: #b43a26; + --miniBg: #d3d6c8; --miniPit: #bcc0ae; + --boardBg: linear-gradient(180deg, #dcdfd2, #cfd2c3); + --boardShadow: inset 0 2px 0 rgba(255, 255, 255, .75), inset 0 -3px 0 rgba(0, 0, 0, .08), 0 22px 44px rgba(40, 50, 40, .2), 0 0 0 1px #bcc0ae; + --pitShadow: inset 0 7px 14px rgba(40, 50, 40, .38), inset 0 -2px 0 rgba(255, 255, 255, .55), 0 1px 0 rgba(255, 255, 255, .75); + --stoneShadow: 0 3px 4px rgba(30, 40, 30, .35); + color-scheme: light; +} + +html, body, #main { min-height: 100%; } + +body { + margin: 0; + background: var(--bg); + color: var(--ink); + font-family: 'Bricolage Grotesque', system-ui, sans-serif; + transition: background .3s, color .3s; +} + +#main { display: flex; flex-direction: column; min-height: 100vh; } + +a { color: inherit; } + +/* Page */ + +.page { display: flex; flex-direction: column; flex: 1; min-width: 0; } + +.wrap { + width: 100%; + max-width: 1180px; + margin: 0 auto; + padding: 28px 32px 56px; + box-sizing: border-box; + display: flex; + flex-direction: column; + gap: 26px; +} + +/* Header */ + +.hdr { + position: sticky; + top: 0; + z-index: 10; + display: flex; + align-items: center; + gap: 12px; + padding: 12px 32px; + background: var(--bg); + border-bottom: 1px solid var(--line); + transition: background .3s; +} + +.logo { + display: flex; + align-items: center; + gap: 10px; + margin-right: auto; + min-width: 0; + color: var(--ink); + text-decoration: none; + font-weight: 800; + font-size: 24px; + letter-spacing: -.02em; +} + +.logo svg { flex: none; } + +.logo span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + +.themes { display: flex; gap: 2px; padding: 3px; border-radius: 26px; background: var(--surface); } + +.sw { + width: 44px; + height: 44px; + border-radius: 50%; + border: none; + padding: 0; + background: transparent; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; +} + +.sw span { width: 22px; height: 22px; border-radius: 50%; box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .25); } + +.sw.on { background: var(--surface2); box-shadow: inset 0 0 0 2px var(--accent); } + +/* Buttons */ + +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + min-height: 44px; + padding: 0 18px; + border-radius: 22px; + border: none; + font: inherit; + font-weight: 700; + font-size: 15px; + cursor: pointer; + text-decoration: none; + white-space: nowrap; + box-sizing: border-box; +} + +.btn.pri { background: var(--accent); color: var(--onAccent); } + +.btn.ghost { background: transparent; color: var(--ink); box-shadow: inset 0 0 0 1.5px var(--line); } + +.btn.icon { width: 44px; padding: 0; } + +.btn:disabled { opacity: .4; cursor: default; } + +.sw:focus-visible, .btn:focus-visible, .card:focus-visible, .logo:focus-visible { + outline: 3px solid var(--accent); + outline-offset: 3px; +} + +/* Pills, cards */ + +.pill { + display: inline-flex; + align-items: center; + gap: 6px; + height: 26px; + padding: 0 10px; + border-radius: 13px; + font-size: 12px; + font-weight: 700; + background: var(--surface2); + color: var(--ink); + white-space: nowrap; +} + +.pill.live, .pill.lose { color: var(--live); } + +.pill.live::before { + content: ''; + width: 7px; + height: 7px; + border-radius: 50%; + background: currentColor; + animation: kpulse 1.6s ease-in-out infinite; +} + +.pill.win { background: var(--accent); color: var(--onAccent); } + +.panel { border-radius: 24px; background: var(--surface); box-shadow: inset 0 0 0 1px var(--line); } + +.dot { width: 8px; height: 8px; flex: none; border-radius: 50%; background: currentColor; animation: kpulse 1.6s ease-in-out infinite; } + +@keyframes kpulse { + 0%, 100% { opacity: 1; transform: scale(1); } + 50% { opacity: .35; transform: scale(.7); } +} + +.visually-hidden { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip: rect(0 0 0 0); + white-space: nowrap; +} + +/* Account menu (@szhsin/react-menu) */ + +.szh-menu { + background: var(--surface); + color: var(--ink); + border-radius: 16px; + padding: 6px; + box-shadow: inset 0 0 0 1px var(--line), 0 16px 30px rgba(0, 0, 0, .25); + font-family: 'Bricolage Grotesque', system-ui, sans-serif; +} + +.szh-menu__header { color: var(--muted); font-weight: 700; text-transform: none; font-size: 13px; } + +.szh-menu__item { border-radius: 10px; min-height: 40px; font-weight: 600; color: var(--ink); } + +.szh-menu__item--hover { background: var(--surface2); } + +/* Dialogs (sweetalert) */ + +.swal-modal { background: var(--surface); border-radius: 24px; font-family: 'Bricolage Grotesque', system-ui, sans-serif; } + +.swal-title, .swal-text { color: var(--ink); } + +.swal-button { border-radius: 22px; font-family: inherit; } + +@media (max-width: 640px) { + .hdr { padding: 10px 16px; gap: 8px; } + .logo { font-size: 20px; gap: 8px; } + .logo svg { width: 28px; height: 28px; } + .themes .sw { width: 40px; height: 40px; } + .hide-m { display: none !important; } + .ic-m { width: 44px; padding: 0; } + .wrap { padding: 16px 16px 32px; gap: 16px; } +} + +/* The account button makes the header too wide for the name on phones. */ +@media (max-width: 420px) { + .hdr.has-account .logo span { display: none; } +} + +@media (max-width: 360px) { + .logo span { display: none; } +} diff --git a/apps/frontend/src/theme/themes.ts b/apps/frontend/src/theme/themes.ts new file mode 100644 index 0000000..09baab5 --- /dev/null +++ b/apps/frontend/src/theme/themes.ts @@ -0,0 +1,94 @@ +import { Theme } from "@mancala/core"; + +// The web app's themes. Pages are coloured by the CSS variables of the theme's class (styles/app.css); the colours +// here are for code that still reads core's Theme, such as PitAnimator. +export type WebTheme = Theme & { + // CSS class on . + className: string; + // Translation key of the theme's name. + labelKey: string; + // Colours of the theme's swatch. + swatch: { board: string, accent: string }; +}; + +const walnut: WebTheme = { + id: "ceviz", + name: "Ceviz", + className: "t-ceviz", + labelKey: "ThemeWalnut", + swatch: { board: "#6e4a30", accent: "#f2b84b" }, + themePreviewColor: "#6e4a30", + background: "#1f1712", + appBarBgColor: "#2a2019", + textColor: "#2a1a0a", + textLightColor: "#f6ead8", + playerTurnColor: "#f2b84b", + boardColor: "#6e4a30", + pitColor: "#2c1d13", + pitSelectedColor: "#f2b84b", + stoneColor: "#e39a32", + stoneLightColor: "#fff1c9", + pitGameMoveAnimateColor: "#f2b84b", + pitEmptyPitAnimateColor: "#ff8a73", + pitLastStoneInBankPitAnimateColor: "#f2b84b", + pitGetRivalStonePitAnimateColor: "#ff8a73", +}; + +const night: WebTheme = { + id: "gece", + name: "Gece", + className: "t-gece", + labelKey: "ThemeNight", + swatch: { board: "#1d2a4d", accent: "#e2b36a" }, + themePreviewColor: "#1d2a4d", + background: "#0c1220", + appBarBgColor: "#141c30", + textColor: "#241707", + textLightColor: "#eef1f8", + playerTurnColor: "#e2b36a", + boardColor: "#1d2a4d", + pitColor: "#0b1020", + pitSelectedColor: "#e2b36a", + stoneColor: "#dfe3ee", + stoneLightColor: "#ffffff", + pitGameMoveAnimateColor: "#e2b36a", + pitEmptyPitAnimateColor: "#ff8a7a", + pitLastStoneInBankPitAnimateColor: "#e2b36a", + pitGetRivalStonePitAnimateColor: "#ff8a7a", +}; + +const jade: WebTheme = { + id: "yesim", + name: "Yeşim", + className: "t-yesim", + labelKey: "ThemeJade", + swatch: { board: "#cfd2c3", accent: "#2a7650" }, + themePreviewColor: "#cfd2c3", + background: "#e9ebe3", + appBarBgColor: "#f7f8f3", + textColor: "#1d2721", + textLightColor: "#ffffff", + playerTurnColor: "#2a7650", + boardColor: "#cfd2c3", + pitColor: "#c3c7b6", + pitSelectedColor: "#2a7650", + stoneColor: "#3f9f6f", + stoneLightColor: "#e2ffee", + pitGameMoveAnimateColor: "#2a7650", + pitEmptyPitAnimateColor: "#b43a26", + pitLastStoneInBankPitAnimateColor: "#2a7650", + pitGetRivalStonePitAnimateColor: "#b43a26", +}; + +// The first one is the default. +export const webThemes: WebTheme[] = [walnut, night, jade]; + +export function getWebTheme(theme: Theme): WebTheme { + return webThemes.find((webTheme) => webTheme.id === theme.id) ?? walnut; +} + +export function applyThemeClass(theme: Theme) { + const root = document.documentElement; + webThemes.forEach((webTheme) => root.classList.remove(webTheme.className)); + root.classList.add(getWebTheme(theme).className); +} diff --git a/docs/specs/web-redesign/tasks.md b/docs/specs/web-redesign/tasks.md index 4d0e2fc..e79371d 100644 --- a/docs/specs/web-redesign/tasks.md +++ b/docs/specs/web-redesign/tasks.md @@ -5,7 +5,7 @@ Work through the tasks in order; each ends with its checks passing and a commit. - [x] 1. **Core**: `ThemeManager` takes its themes; `PitViewModel.highlight` set by `PitAnimator`; `getSowingPath` with unit tests; new texts (en, tr). - [x] 2. **Backend**: `GET /games/popular?kind=finished`; test. -- [ ] 3. **Web app shell**: the three themes, font, base styles, `AppHeader` (logo, theme swatches, account menu, +- [x] 3. **Web app shell**: the three themes, font, base styles, `AppHeader` (logo, theme swatches, account menu, action) on every page; restyle the lobby, privacy page, loading and connection panels. - [ ] 4. **Web app board**: `BoardView`, `PitView`, `StoreView`, stones, upright layout, highlights and move preview. - [ ] 5. **Web app game page**: players, turn, clocks, game-over card, move list and review banner.