diff --git a/AGENTS.md b/AGENTS.md index 1792ff0..f5f5215 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,8 +24,8 @@ Online Mancala (the Turkish *Mangala* rules) with a web app, an Android/iOS app Specs that exist: `keycloak-auth` (login, guests, tokens), `game-history` (saved games, live games, replay), `game-clock` (5 minutes per player, ending games on time), `web-redesign` (the web app's look: themes, board, pages), `quarkus-backend` (the backend's move from Node to Java and Quarkus), `game-bot` (playing against a bot -at four levels), `tutorial` (rules page, lessons, hints in bot games) and `rematch` (rematches, alternating first -moves, the series strip). +at four levels), `tutorial` (rules page, lessons, hints in bot games), `rematch` (rematches, alternating first +moves, the series strip) and `theme-polish` (the login pages in the app's themes, the web app's messages). ## Running locally @@ -142,9 +142,13 @@ which helps to check what the server holds. - The guest authenticator uses an internal Keycloak SPI: keep the version in `extension/pom.xml` and `Dockerfile` in sync and run `mvn test` when upgrading. - Keycloak sets `Secure` cookies; browsers send them to `http://localhost`, simple HTTP clients do not. -- The login theme (`apps/keycloak/themes/mancala`) is only CSS on top of `keycloak.v2`. Keycloak in `start` mode caches - themes and keeps gzipped resources in `data/tmp/kc-gzip-cache` across restarts; see `apps/keycloak/README.md` for - reloading it in a running container. +- The login theme (`apps/keycloak/themes/mancala`) is CSS and one script on top of `keycloak.v2`. The script takes the + theme from the `mancala_theme` cookie the web app writes (`saveThemeCookie`); the three themes' colors are copied + from `apps/frontend/src/styles/app.css`, so change both. Browsers cache theme files under a path that only changes + with Keycloak's version, for an hour (`KC_SPI_THEME__STATIC_MAX_AGE` in the Dockerfile); after a change, check in a + private window or rename the file. Keycloak in `start` mode caches themes and keeps gzipped + resources in `data/tmp/kc-gzip-cache` across restarts; see `apps/keycloak/README.md` for reloading it in a running + container. **Mobile** - `apps/mobile/release_android.sh` contains signing passwords (see `TODO.md`); do not copy them anywhere. diff --git a/TODO.md b/TODO.md index dcbdedd..edd8643 100644 --- a/TODO.md +++ b/TODO.md @@ -35,7 +35,6 @@ The web app has game history (see `docs/specs/game-history`); the mobile app nee - [ ] Statistics for logged in users (wins, losses), now that games are saved - [ ] Social logins (Keycloak identity providers) - [ ] Keycloak email theme in the game's look (password reset emails still use Keycloak's) -- [ ] Login pages follow the system color scheme; they could follow the theme chosen in the app instead - [ ] Restrict the Keycloak admin console in production ## Game history @@ -80,6 +79,7 @@ See `docs/specs/tutorial`. See `docs/specs/web-redesign`. - [ ] Bring the new design to the mobile app (themes, board, game screen) +- [ ] The design's "Sound effects" switch in the settings menu, once the app has sounds ## Rematch @@ -94,6 +94,21 @@ See `docs/specs/rematch`. ## Deployment +- [ ] Document the deployment (e.g. `docs/deployment.md`, or a `Jenkinsfile` in the repo). Today it is a Jenkins + job at `ci.playmancala.app` whose shell steps live only in Jenkins; it still builds from `apps/backend` and + `apps/frontend` and runs `cd apps && docker compose up -d`. For the Java backend it needs: + - `docker build -f apps/backend/Dockerfile -t jhalitaksoy/mancala-backend:latest .` from the repo root + - `docker build -f apps/frontend/Dockerfile` with `--build-arg` `SERVER_ADDRESS=https://api.playmancala.app`, + `WS_SERVER_ADDRESS=wss://api.playmancala.app`, `KEYCLOAK_URL=https://auth.playmancala.app` + - `docker build -t jhalitaksoy/mancala-keycloak:latest apps/keycloak` + - `docker compose --env-file /mancala.env up -d` in `apps`, with the env file kept outside the Jenkins + workspace (`POSTGRES_PASSWORD`, `KEYCLOAK_DB_PASSWORD`, `MANCALA_DB_PASSWORD`, `KEYCLOAK_ADMIN_USERNAME`, + `KEYCLOAK_ADMIN_PASSWORD`, `KEYCLOAK_HOSTNAME=https://auth.playmancala.app`, `LEGACY_USERKEY_AUTH`) + - Once, by hand: the `auth.playmancala.app` block of `config/nginx.conf` on the server and an nginx reload. + DNS and the certificate for `auth.playmancala.app` already exist + - If Jenkins runs in a container with the host's Docker socket, the `./postgres/init` bind mount points at a + path the host does not have and the databases are not created; check before the first deploy + - Which branch the job builds, and whether it builds on push - [ ] Build the backend and frontend images from the repo root in CI: `docker build -f apps/backend/Dockerfile .` and `docker build -f apps/frontend/Dockerfile .` - [ ] Pass `--build-arg KEYCLOAK_URL=https://auth.playmancala.app` when building the frontend image diff --git a/apps/frontend/src/MancalaApp.tsx b/apps/frontend/src/MancalaApp.tsx index 4c0be81..168e5eb 100644 --- a/apps/frontend/src/MancalaApp.tsx +++ b/apps/frontend/src/MancalaApp.tsx @@ -22,6 +22,7 @@ import Util from './util/Util'; import { server } from './const/config'; import { useTranslation } from 'react-i18next'; import { applyThemeClass } from './theme/themes'; +import { AuthUserContext } from './context/AuthUserContext'; import './styles/app.css'; const context = initContext(); @@ -122,7 +123,7 @@ const MancalaApp: FunctionComponent = () => { return ( <> - {hasSession && + {hasSession && } /> @@ -138,7 +139,7 @@ const MancalaApp: FunctionComponent = () => { } /> - } + } {Util.getTextByConnectionState(context, connectionState, t)} diff --git a/apps/frontend/src/components/BotLevelMenu.tsx b/apps/frontend/src/components/BotLevelMenu.tsx index 6a43278..6625fa5 100644 --- a/apps/frontend/src/components/BotLevelMenu.tsx +++ b/apps/frontend/src/components/BotLevelMenu.tsx @@ -8,13 +8,13 @@ import { BotIcon } from "./Icons"; import "@szhsin/react-menu/dist/index.css"; import "@szhsin/react-menu/dist/transitions/slide.css" -// "Play with bot" in the header: a menu of the levels, the one chosen last marked. -const BotLevelMenu: FunctionComponent<{ context: Context, onChoose: (level: BotLevel) => void }> = ({ context, onChoose }) => { +// "Play with bot": a menu of the levels, the one chosen last marked. `className` styles the button: the header's hides on phones, the home page's row of actions shows it there. +const BotLevelMenu: FunctionComponent<{ context: Context, onChoose: (level: BotLevel) => void, className?: string, label?: string }> = ({ context, onChoose, className, label }) => { const { t } = useTranslation(); const lastLevel = getLastBotLevel(context.storage); const menuButton = ( - ); const choose = (level: BotLevel) => { diff --git a/apps/frontend/src/components/Icons.tsx b/apps/frontend/src/components/Icons.tsx index 37f93d1..99f7a93 100644 --- a/apps/frontend/src/components/Icons.tsx +++ b/apps/frontend/src/components/Icons.tsx @@ -24,10 +24,6 @@ export const PlusIcon = ({ size = 20 }: IconProps) => ( ); -export const ProfileIcon = ({ size = 20 }: IconProps) => ( - -); - export const LeaveIcon = ({ size = 20 }: IconProps) => ( ); @@ -72,6 +68,10 @@ export const BotIcon = ({ size = 20 }: IconProps) => ( ); +export const SettingsIcon = ({ size = 22 }: IconProps) => ( + +); + export const HelpIcon = ({ size = 20 }: IconProps) => ( ); diff --git a/apps/frontend/src/components/headerbar/AccountMenu.tsx b/apps/frontend/src/components/headerbar/AccountMenu.tsx index f971cf6..5fc2446 100644 --- a/apps/frontend/src/components/headerbar/AccountMenu.tsx +++ b/apps/frontend/src/components/headerbar/AccountMenu.tsx @@ -4,7 +4,7 @@ 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 { getInitials } from "../../util/PlayerText"; import "@szhsin/react-menu/dist/index.css"; import "@szhsin/react-menu/dist/transitions/slide.css" @@ -12,9 +12,10 @@ import "@szhsin/react-menu/dist/transitions/slide.css" const AccountMenu: FunctionComponent<{ context: Context, authUser: AuthUser }> = ({ context, authUser }) => { const { t } = useTranslation(); const authService = context.authService; - const menuButton = ( - ); const menuItems = authUser.isGuest ? [ @@ -25,7 +26,7 @@ const AccountMenu: FunctionComponent<{ context: Context, authUser: AuthUser }> = ]; return ( - {getPlayerDisplayName(authUser, t)} + {name} {menuItems} ); diff --git a/apps/frontend/src/components/headerbar/AppHeader.tsx b/apps/frontend/src/components/headerbar/AppHeader.tsx index bb22127..a8dca1f 100644 --- a/apps/frontend/src/components/headerbar/AppHeader.tsx +++ b/apps/frontend/src/components/headerbar/AppHeader.tsx @@ -1,30 +1,33 @@ import * as React from "react"; -import { FunctionComponent } from "react"; +import { FunctionComponent, useContext } from "react"; import { Link } from "react-router-dom"; -import { AuthUser } from "@mancala/core"; import { useTranslation } from "react-i18next"; import { Context } from "../../context/context"; +import { AuthUserContext } from "../../context/AuthUserContext"; import { HelpIcon, LogoIcon } from "../Icons"; import AccountMenu from "./AccountMenu"; -import ThemeSwatches from "./ThemeSwatches"; +import SettingsMenu from "./SettingsMenu"; -// The header of every page: logo and name, theme swatches, how to play, the account menu when a user is given, and -// the page's main action (children). How to play opens the rules page, or calls `onHelp` (the game page shows the -// rules over the game instead). -const AppHeader: FunctionComponent<{ context: Context, authUser?: AuthUser, onHelp?: () => void }> = ({ context, authUser, onHelp, children }) => { +// The header of every page: logo and name, the page's actions (children), how to play, settings (the themes) and the +// account menu once the session has started. How to play opens the rules page, or calls `onHelp` (the game page +// shows the rules over the game instead). +const AppHeader: FunctionComponent<{ context: Context, onHelp?: () => void }> = ({ context, onHelp, children }) => { const { t } = useTranslation(); + const authUser = useContext(AuthUserContext); return ( -
+
{t("Mancala")} - - {onHelp - ? - : } - {authUser && } {children} +
+ {onHelp + ? + : } + + {authUser && } +
); }; diff --git a/apps/frontend/src/components/headerbar/SettingsMenu.tsx b/apps/frontend/src/components/headerbar/SettingsMenu.tsx new file mode 100644 index 0000000..931de6a --- /dev/null +++ b/apps/frontend/src/components/headerbar/SettingsMenu.tsx @@ -0,0 +1,37 @@ +import { Menu, MenuHeader, MenuItem } from "@szhsin/react-menu"; +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"; +import { SettingsIcon } from "../Icons"; +import "@szhsin/react-menu/dist/index.css"; +import "@szhsin/react-menu/dist/transitions/slide.css" + +// Settings in the header: the three themes, each with its swatch, the chosen one checked. +const SettingsMenu: FunctionComponent<{ context: Context }> = ({ context }) => { + const { t } = useTranslation(); + const current = getWebTheme(context.themeManager.theme); + const menuButton = ({ open }: { open: boolean }) => ( + + ); + return ( + + {t("Theme")} + {webThemes.map((theme) => ( + (context.themeManager.theme = theme)}> + + {t(theme.nameKey)} + + ))} + + ); +}; + +export default SettingsMenu; diff --git a/apps/frontend/src/components/headerbar/ThemeSwatches.tsx b/apps/frontend/src/components/headerbar/ThemeSwatches.tsx deleted file mode 100644 index add43d9..0000000 --- a/apps/frontend/src/components/headerbar/ThemeSwatches.tsx +++ /dev/null @@ -1,26 +0,0 @@ -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/context/AuthUserContext.ts b/apps/frontend/src/context/AuthUserContext.ts new file mode 100644 index 0000000..50b0951 --- /dev/null +++ b/apps/frontend/src/context/AuthUserContext.ts @@ -0,0 +1,5 @@ +import * as React from "react"; +import { AuthUser } from "@mancala/core"; + +// The signed-in user (a guest too), once the session has started; the header's account menu reads it on every page. +export const AuthUserContext = React.createContext(undefined); diff --git a/apps/frontend/src/routes/Home.tsx b/apps/frontend/src/routes/Home.tsx index cb7f733..2cfaaba 100644 --- a/apps/frontend/src/routes/Home.tsx +++ b/apps/frontend/src/routes/Home.tsx @@ -52,13 +52,18 @@ const Home: FunctionComponent<{ return ( - - -
+ {/* On phones the header's actions move here. */} +
+ + +
{activeGame && authUser && ( .btn { width: 100%; } + /* Dialogs (sweetalert) */ .swal-modal { background: var(--surface); border-radius: 24px; font-family: 'Bricolage Grotesque', system-ui, sans-serif; } @@ -228,17 +274,38 @@ a { color: inherit; } .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; } + .show-m { display: grid !important; } + .hdr-tools { margin-left: 4px; gap: 2px; } .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; } } + +/* Short messages (util/Notyf.ts): below the header at the top centre, as an inverted chip of the theme. */ + +.notyf { padding: 84px 16px 16px; } +.notyf .notyf__toast { + max-width: min(420px, 100%); + width: auto; + padding: 0 18px; + border-radius: 18px; + color: var(--bg); + font-family: 'Bricolage Grotesque', system-ui, sans-serif; + font-weight: 700; + font-size: 15px; + box-shadow: 0 12px 30px rgba(0, 0, 0, .3); + cursor: pointer; +} +.notyf .notyf__toast--upper { margin-bottom: 10px; } +.notyf .notyf__wrapper { padding: 11px 0; } +.notyf .notyf__icon { display: none; } +.notyf .notyf__message { line-height: 1.35; text-align: center; } + +@media (max-width: 640px) { + .notyf { padding-top: 76px; } +} + diff --git a/apps/frontend/src/theme/themes.ts b/apps/frontend/src/theme/themes.ts index 09baab5..a4f8443 100644 --- a/apps/frontend/src/theme/themes.ts +++ b/apps/frontend/src/theme/themes.ts @@ -7,6 +7,8 @@ export type WebTheme = Theme & { className: string; // Translation key of the theme's name. labelKey: string; + // The name alone ("Walnut"), as the settings menu lists it. + nameKey: string; // Colours of the theme's swatch. swatch: { board: string, accent: string }; }; @@ -16,6 +18,7 @@ const walnut: WebTheme = { name: "Ceviz", className: "t-ceviz", labelKey: "ThemeWalnut", + nameKey: "ThemeWalnutName", swatch: { board: "#6e4a30", accent: "#f2b84b" }, themePreviewColor: "#6e4a30", background: "#1f1712", @@ -39,6 +42,7 @@ const night: WebTheme = { name: "Gece", className: "t-gece", labelKey: "ThemeNight", + nameKey: "ThemeNightName", swatch: { board: "#1d2a4d", accent: "#e2b36a" }, themePreviewColor: "#1d2a4d", background: "#0c1220", @@ -62,6 +66,7 @@ const jade: WebTheme = { name: "Yeşim", className: "t-yesim", labelKey: "ThemeJade", + nameKey: "ThemeJadeName", swatch: { board: "#cfd2c3", accent: "#2a7650" }, themePreviewColor: "#cfd2c3", background: "#e9ebe3", @@ -87,8 +92,24 @@ export function getWebTheme(theme: Theme): WebTheme { return webThemes.find((webTheme) => webTheme.id === theme.id) ?? walnut; } +// Puts the theme's class on and remembers the theme for the login pages. export function applyThemeClass(theme: Theme) { const root = document.documentElement; webThemes.forEach((webTheme) => root.classList.remove(webTheme.className)); root.classList.add(getWebTheme(theme).className); + saveThemeCookie(theme); +} + +const THEME_COOKIE = "mancala_theme"; +const COOKIE_MAX_AGE_SECONDS = 365 * 24 * 60 * 60; + +// The Keycloak login pages are on another origin and read the theme from this cookie (docs/specs/theme-polish). +// On localhost a host-only cookie reaches every port; on a domain the cookie is set for its last two labels, so +// playmancala.app shares it with auth.playmancala.app. +export function saveThemeCookie(theme: Theme) { + const { hostname, protocol } = window.location; + const isDomain = hostname.includes(".") && !/^[\d.]+$/.test(hostname); + const domain = isDomain ? `; domain=${hostname.split(".").slice(-2).join(".")}` : ""; + const secure = protocol === "https:" ? "; Secure" : ""; + document.cookie = `${THEME_COOKIE}=${getWebTheme(theme).id}; path=/; max-age=${COOKIE_MAX_AGE_SECONDS}; SameSite=Lax${domain}${secure}`; } diff --git a/apps/frontend/src/util/Notyf.ts b/apps/frontend/src/util/Notyf.ts index 753a1f7..ba90d76 100644 --- a/apps/frontend/src/util/Notyf.ts +++ b/apps/frontend/src/util/Notyf.ts @@ -1,4 +1,20 @@ -import { Notyf } from 'notyf'; -import 'notyf/notyf.min.css'; -const notyf = new Notyf(); -export default notyf; \ No newline at end of file +import { Notyf, NotyfEvent } from 'notyf'; +import 'notyf/notyf.min.css'; + +// Short messages at the top centre, below the header, in the theme's colors (styles/app.css). Messages are hints +// about what cannot be done right now, so they carry no icon and go away by themselves or when clicked. +const notyf = new Notyf({ + duration: 2500, + ripple: false, + position: { x: 'center', y: 'top' }, + types: [{ type: 'error', background: 'var(--ink)', icon: false }], +}); + +const messages = { + error(message: string) { + const notification = notyf.error(message); + notification.on(NotyfEvent.Click, () => notyf.dismiss(notification)); + }, +}; + +export default messages; diff --git a/apps/keycloak/Dockerfile b/apps/keycloak/Dockerfile index 01512b6..04a1083 100644 --- a/apps/keycloak/Dockerfile +++ b/apps/keycloak/Dockerfile @@ -19,5 +19,8 @@ COPY --from=builder /opt/keycloak/ /opt/keycloak/ # Imported on startup only if the realm does not exist yet. COPY realm/mancala-realm.json /opt/keycloak/data/import/ COPY themes/ /opt/keycloak/themes/ +# Browsers keep theme resources an hour instead of 30 days: their path only changes with Keycloak's version, so a +# theme change would otherwise reach returning players a month late. +ENV KC_SPI_THEME__STATIC_MAX_AGE=3600 ENTRYPOINT ["/opt/keycloak/bin/kc.sh"] CMD ["start", "--optimized", "--import-realm"] diff --git a/apps/keycloak/README.md b/apps/keycloak/README.md index e96e678..39dc52e 100644 --- a/apps/keycloak/README.md +++ b/apps/keycloak/README.md @@ -35,11 +35,18 @@ console too, or delete the realm and restart Keycloak. ## Login theme -`themes/mancala/login` gives the login, registration and password reset pages the game's look: its app bar and logo, -the board's colors and a board with stones on top of the card. It extends Keycloak's `keycloak.v2` theme and only -adds `resources/css/mancala.css`, so its templates and behaviour stay Keycloak's own and upgrades need no template -changes. The colors come from `LightTheme` and `DarkTheme` in `packages/core/src/theme`; the dark ones are used when -the system prefers a dark color scheme. The realm turns the theme on (`loginTheme`) and offers English and Turkish; +`themes/mancala/login` gives the login, registration and password reset pages the web app's look in its three themes +(Ceviz, Gece, Yeşim): its header, logo and name, font, cards, fields, pill buttons and a board with stones on top of +the card (`docs/specs/theme-polish`). It extends Keycloak's `keycloak.v2` theme and only adds +`resources/css/login.css` and `resources/js/theme.js`, so its templates and behaviour stay Keycloak's own and +upgrades need no template changes. + +The script runs in the page's head and puts the theme's class (`t-ceviz`, `t-gece`, `t-yesim`) on ``: the +theme the player chose in the web app, which the app keeps in the `mancala_theme` cookie (shared by `localhost`'s +ports, and set for `playmancala.app` so `auth.playmancala.app` gets it), or else Ceviz for a dark system color scheme +and Yeşim for a light one. Keycloak's own dark mode is off (`darkMode=false`); the script adds PatternFly's +`pf-v5-theme-dark` for the dark themes. The themes' colors are copied from `apps/frontend/src/styles/app.css`; the logo +and the board are masks (`img/*-mask.svg`) filled with them. The realm turns the theme on (`loginTheme`) and offers English and Turkish; the web app passes its language as `ui_locales`. To try a change without rebuilding the image, copy the theme into the running container, clear Keycloak's gzip cache diff --git a/apps/keycloak/themes/mancala/login/resources/css/login.css b/apps/keycloak/themes/mancala/login/resources/css/login.css new file mode 100644 index 0000000..eabc7d0 --- /dev/null +++ b/apps/keycloak/themes/mancala/login/resources/css/login.css @@ -0,0 +1,420 @@ +/* + * The web app's look for keycloak.v2's login pages (docs/specs/theme-polish). Loaded after keycloak.v2's styles.css. + * js/theme.js puts the player's theme on as t-ceviz, t-gece or t-yesim; their colors are copied from + * apps/frontend/src/styles/app.css, keep them in step. The dark themes also get pf-v5-theme-dark. + */ + +@import url("https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,400;12..96,600;12..96,700;12..96,800&display=swap"); + +.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); --live: #ff8a73; --miniBg: #5b3d28; + 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); --live: #ff8a7a; --miniBg: #1d2a4d; + 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); --live: #b43a26; --miniBg: #d3d6c8; + color-scheme: light; +} + +:root:is(.t-ceviz, .t-gece, .t-yesim) { + --font: 'Bricolage Grotesque', system-ui, sans-serif; + --bar-height: 69px; + + --pf-v5-global--FontFamily--text: var(--font); + --pf-v5-global--FontFamily--heading: var(--font); + --pf-v5-global--Color--100: var(--ink); + --pf-v5-global--Color--200: var(--muted); + --pf-v5-global--BackgroundColor--100: var(--surface); + --pf-v5-global--BackgroundColor--200: var(--bg); + --pf-v5-global--BorderColor--100: var(--line); + --pf-v5-global--link--Color: var(--ink); + --pf-v5-global--link--Color--hover: var(--accent); + --pf-v5-global--primary-color--100: var(--accent); + --pf-v5-global--danger-color--100: var(--live); + --pf-v5-global--danger-color--200: var(--live); +} + +.login-pf body { + background: var(--bg); + color: var(--ink); + font-family: var(--font); +} + +/* Header: the app's header with its logo and name */ + +#kc-header { + position: fixed; + inset: 0 0 auto 0; + z-index: 10; + height: var(--bar-height); + margin: 0; + padding: 0 32px; + box-sizing: border-box; + display: flex; + align-items: center; + background: var(--bg); + border-bottom: 1px solid var(--line); +} + +/* The realm's name is replaced by the app's, which is "Köçürme" in Turkish. */ +#kc-header-wrapper { + display: flex; + align-items: center; + gap: 10px; + margin: 0; + padding: 0; + font-size: 0; + letter-spacing: normal; + text-transform: none; + color: var(--ink) !important; +} + +#kc-header-wrapper::before { + content: ""; + flex: none; + width: 34px; + height: 34px; + background: var(--accent); + -webkit-mask: url("../img/logo-mask.svg") center / contain no-repeat; + mask: url("../img/logo-mask.svg") center / contain no-repeat; +} + +#kc-header-wrapper::after { + content: "Mancala"; + font-family: var(--font); + font-size: 24px; + font-weight: 800; + letter-spacing: -.02em; + line-height: 1; +} + +:lang(tr) #kc-header-wrapper::after { + content: "Köçürme"; +} + +.pf-v5-c-login { + min-height: 100vh; + padding-block-start: calc(var(--bar-height) + 2rem); + padding-block-end: 2rem; +} + +.pf-v5-c-login__container { + max-width: calc(30rem + 32px); + padding-inline: 16px; + grid-template-columns: minmax(0, 30rem); +} + +/* Card: the app's card, with a board at the start of a game on top */ + +.pf-v5-c-login__main { + --pf-v5-c-login__main--BackgroundColor: var(--surface); + --pf-v5-c-login__main--BoxShadow: 0 20px 50px rgba(0, 0, 0, .25); + --pf-v5-c-login__main-footer-band--BorderTopColor: var(--line); + --pf-v5-c-login__main-footer-band--BackgroundColor: transparent; + position: relative; + padding-block-start: 2rem; + border-radius: 28px; + color: var(--ink); +} + +.pf-v5-c-login__main::before, +.pf-v5-c-login__main::after { + content: ""; + display: block; + width: min(calc(100% - 4rem), 280px); + aspect-ratio: 328 / 92; + margin: 0 auto; +} + +.pf-v5-c-login__main::before { + background: var(--miniBg); + -webkit-mask: url("../img/board-mask.svg") center / contain no-repeat; + mask: url("../img/board-mask.svg") center / contain no-repeat; +} + +.pf-v5-c-login__main::after { + position: absolute; + top: 2rem; + left: 50%; + transform: translateX(-50%); + background: var(--accent); + -webkit-mask: url("../img/stones-mask.svg") center / contain no-repeat; + mask: url("../img/stones-mask.svg") center / contain no-repeat; +} + +.pf-v5-c-login__main-header { + --pf-v5-c-login__main-header--PaddingTop: 1.5rem; + border-top: none; +} + +#kc-page-title { + color: var(--ink); + font-family: var(--font); + font-size: 1.6rem; + font-weight: 800; + letter-spacing: -.02em; +} + +.pf-v5-c-login__main hr { + border-color: var(--line); +} + +.pf-v5-c-login__main a:not(.pf-v5-c-button) { + color: var(--ink); + font-weight: 700; + text-decoration: underline; + text-decoration-color: var(--accentSoft); + text-underline-offset: 3px; +} + +.pf-v5-c-login__main a:not(.pf-v5-c-button):hover { + color: var(--accent); + text-decoration-color: currentColor; +} + +.pf-v5-c-form__label, +.pf-v5-c-form__label-text { + color: var(--ink); + font-weight: 700; +} + +.pf-v5-c-login__main .pf-v5-c-helper-text, +.pf-v5-c-login__main .pf-v5-c-check__label, +.pf-v5-c-login__main-footer-band { + color: var(--muted); +} + +/* Inputs */ + +.pf-v5-c-form-control { + --pf-v5-c-form-control--BackgroundColor: var(--surface2); + --pf-v5-c-form-control--Color: var(--ink); + --pf-v5-c-form-control--m-placeholder--Color: var(--muted); + --pf-v5-c-form-control--m-readonly--BackgroundColor: var(--bg); + --pf-v5-c-form-control__toggle-icon--Color: var(--ink); + --pf-v5-c-form-control--PaddingLeft: 1rem; + --pf-v5-c-form-control--PaddingRight: 1rem; + min-height: 46px; + border: 1.5px solid var(--line); + border-radius: 14px; + font-family: var(--font); +} + +.pf-v5-c-form-control::before, +.pf-v5-c-form-control::after { + content: none; +} + +.pf-v5-c-form-control > :is(input, select, textarea) { + font-family: inherit; + color: var(--ink); + background-color: transparent; + border-radius: inherit; +} + +.pf-v5-c-form-control > select { + padding-inline-start: 1rem; +} + +.pf-v5-c-form-control > select option { + background: var(--surface); + color: var(--ink); +} + +.pf-v5-c-form-control > :is(input, select, textarea):focus { + outline: none; +} + +.pf-v5-c-form-control:hover { + border-color: var(--muted); +} + +.pf-v5-c-form-control:focus-within { + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accentSoft); +} + +.pf-v5-c-form-control.pf-m-error { + border-color: var(--live); +} + +.pf-v5-c-form-control.pf-m-error .pf-v5-c-form-control__icon.pf-m-status { + color: var(--live); +} + +.pf-v5-c-login__main .pf-v5-c-helper-text__item.pf-m-error, +.pf-v5-c-login__main .pf-v5-c-helper-text__item.pf-m-error .pf-v5-c-helper-text__item-text, +.pf-v5-c-login__main .pf-v5-c-helper-text__item-text.pf-m-error { + color: var(--live); + font-weight: 700; +} + +/* The language select in the card's header */ + +.pf-v5-c-login__main-header-utilities .pf-v5-c-form-control { + min-height: 40px; + border-radius: 20px; +} + +/* Password field and its show button form one field: the group has the border */ + +.pf-v5-c-input-group { + border: 1.5px solid var(--line); + border-radius: 14px; + background: var(--surface2); + overflow: hidden; +} + +.pf-v5-c-input-group:hover { + border-color: var(--muted); +} + +.pf-v5-c-input-group:focus-within { + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accentSoft); +} + +.pf-v5-c-input-group .pf-v5-c-form-control, +.pf-v5-c-input-group .pf-v5-c-form-control:focus-within { + border: none; + border-radius: 0; + box-shadow: none; +} + +.pf-v5-c-input-group .pf-v5-c-button.pf-m-control { + --pf-v5-c-button--BorderRadius: 0; + --pf-v5-c-button--after--BorderRadius: 0; + border-radius: 0; +} + +.pf-v5-c-input-group .pf-v5-c-button.pf-m-control::after { + border: none; +} + +/* Buttons: the app's pills */ + +.pf-v5-c-button { + --pf-v5-c-button--BorderRadius: 23px; + --pf-v5-c-button--after--BorderRadius: 23px; + --pf-v5-c-button--FontWeight: 700; + + --pf-v5-c-button--m-primary--BackgroundColor: var(--accent); + --pf-v5-c-button--m-primary--hover--BackgroundColor: var(--accent); + --pf-v5-c-button--m-primary--focus--BackgroundColor: var(--accent); + --pf-v5-c-button--m-primary--active--BackgroundColor: var(--accent); + --pf-v5-c-button--m-primary--Color: var(--onAccent); + --pf-v5-c-button--m-primary--hover--Color: var(--onAccent); + --pf-v5-c-button--m-primary--focus--Color: var(--onAccent); + --pf-v5-c-button--m-primary--active--Color: var(--onAccent); + + --pf-v5-c-button--m-secondary--BackgroundColor: transparent; + --pf-v5-c-button--m-secondary--Color: var(--ink); + --pf-v5-c-button--m-secondary--hover--Color: var(--ink); + --pf-v5-c-button--m-secondary--focus--Color: var(--ink); + --pf-v5-c-button--m-secondary--active--Color: var(--ink); + --pf-v5-c-button--m-secondary--after--BorderColor: var(--line); + --pf-v5-c-button--m-secondary--hover--after--BorderColor: var(--muted); + --pf-v5-c-button--m-secondary--focus--after--BorderColor: var(--muted); + --pf-v5-c-button--m-secondary--active--after--BorderColor: var(--muted); + --pf-v5-c-button--m-secondary--hover--BackgroundColor: transparent; + --pf-v5-c-button--m-secondary--focus--BackgroundColor: transparent; + --pf-v5-c-button--m-secondary--active--BackgroundColor: transparent; + + --pf-v5-c-button--m-link--Color: var(--ink); + --pf-v5-c-button--m-link--hover--Color: var(--accent); + --pf-v5-c-button--m-link--focus--Color: var(--accent); + --pf-v5-c-button--m-link--active--Color: var(--accent); + + --pf-v5-c-button--m-control--BackgroundColor: var(--surface2); + --pf-v5-c-button--m-control--hover--BackgroundColor: var(--surface2); + --pf-v5-c-button--m-control--focus--BackgroundColor: var(--surface2); + --pf-v5-c-button--m-control--active--BackgroundColor: var(--surface2); + --pf-v5-c-button--m-control--Color: var(--ink); + --pf-v5-c-button--m-control--hover--Color: var(--ink); + --pf-v5-c-button--m-control--focus--Color: var(--ink); + --pf-v5-c-button--m-control--active--Color: var(--ink); + --pf-v5-c-button--m-control--after--BorderColor: var(--line); + --pf-v5-c-button--m-control--hover--after--BorderColor: var(--muted); + font-family: var(--font); +} + +.pf-v5-c-button.pf-m-primary { + min-height: 46px; + font-size: 15px; +} + +.pf-v5-c-button.pf-m-primary:hover { + filter: brightness(1.06); +} + +/* Secondary buttons are the app's ghost buttons: a thin line, a stronger one on hover */ +.pf-v5-c-button.pf-m-secondary::after { + border: 1.5px solid var(--line); +} + +.pf-v5-c-button.pf-m-secondary:hover::after { + border-color: var(--muted); +} + +.pf-v5-c-button:focus-visible { + outline: 3px solid var(--accent); + outline-offset: 3px; +} + +.pf-v5-c-check__input { + accent-color: var(--accent); +} + +/* Messages: info, success, warning and errors above the form */ + +.pf-v5-c-alert { + --pf-v5-c-alert--BackgroundColor: var(--surface2); + --pf-v5-c-alert--BoxShadow: none; + --pf-v5-c-alert--BorderTopWidth: 0; + --pf-v5-c-alert__title--Color: var(--ink); + --pf-v5-c-alert__icon--Color: var(--accent); + --pf-v5-c-alert--m-danger__icon--Color: var(--live); + --pf-v5-c-alert--m-danger__title--Color: var(--ink); + --pf-v5-c-alert--m-warning__title--Color: var(--ink); + --pf-v5-c-alert--m-success__title--Color: var(--ink); + --pf-v5-c-alert--m-info__title--Color: var(--ink); + border-radius: 14px; +} + +.pf-v5-c-alert.pf-m-danger { + box-shadow: inset 4px 0 0 var(--live); +} + +@media (max-width: 640px) { + :root:is(.t-ceviz, .t-gece, .t-yesim) { + --bar-height: 61px; + } + + #kc-header { + padding: 0 16px; + } + + #kc-header-wrapper::before { + width: 28px; + height: 28px; + } + + #kc-header-wrapper::after { + font-size: 20px; + } + + .pf-v5-c-login__main { + border-radius: 24px; + } +} diff --git a/apps/keycloak/themes/mancala/login/resources/css/mancala.css b/apps/keycloak/themes/mancala/login/resources/css/mancala.css deleted file mode 100644 index 86d0b7d..0000000 --- a/apps/keycloak/themes/mancala/login/resources/css/mancala.css +++ /dev/null @@ -1,294 +0,0 @@ -/* - * Mancala look for keycloak.v2's login pages. Loaded after keycloak.v2's styles.css. - * Colors come from LightTheme and DarkTheme in packages/core/src/theme; text colors are darker (or lighter) than - * the game's where the game's own would not be readable on the board color. - * keycloak.v2 adds pf-v5-theme-dark to when the system prefers a dark color scheme. - */ - -:root { - --mancala-bg: #bbbbbb; - --mancala-bar: #7b7b7b; - --mancala-bar-text: #ebebeb; - --mancala-board: #9b9b9b; - --mancala-pit: #c4c4c4; - --mancala-pit-readonly: #b0b0b0; - --mancala-pit-edge: #4d4d4d; - --mancala-pit-shadow: rgba(0, 0, 0, 0.22); - --mancala-stone: #5b5b5b; - --mancala-stone-hover: #474747; - --mancala-stone-text: #f2f2f2; - --mancala-ink: #262626; - --mancala-ink-soft: #333333; - --mancala-ink-strong: #000000; - --mancala-hover: rgba(0, 0, 0, 0.08); - --mancala-divider: #6b6b6b; - --mancala-danger: #6b0900; - --mancala-board-image: url("../img/board.svg"); - --mancala-bar-height: 57px; - --mancala-font: serif; - - --pf-v5-global--FontFamily--text: var(--mancala-font); - --pf-v5-global--FontFamily--heading: var(--mancala-font); - --pf-v5-global--Color--100: var(--mancala-ink); - --pf-v5-global--Color--200: var(--mancala-ink-soft); - --pf-v5-global--link--Color: var(--mancala-ink); - --pf-v5-global--link--Color--hover: var(--mancala-ink-strong); - --pf-v5-global--danger-color--100: var(--mancala-danger); -} - -:root.pf-v5-theme-dark { - --mancala-bg: #414141; - --mancala-bar: #313131; - --mancala-bar-text: #aaaaaa; - --mancala-board: #313131; - --mancala-pit: #606060; - --mancala-pit-readonly: #4b4b4b; - --mancala-pit-edge: #8a8a8a; - --mancala-pit-shadow: rgba(0, 0, 0, 0.45); - --mancala-stone: #aaaaaa; - --mancala-stone-hover: #bdbdbd; - --mancala-stone-text: #1e1e1e; - --mancala-ink: #e0e0e0; - --mancala-ink-soft: #bbbbbb; - --mancala-ink-strong: #ffffff; - --mancala-hover: rgba(255, 255, 255, 0.08); - --mancala-divider: #555555; - --mancala-danger: #ff8a80; - --mancala-board-image: url("../img/board-dark.svg"); -} - -.login-pf body { - background: var(--mancala-bg); - font-family: var(--mancala-font); -} - -/* Header: the game's app bar with its logo and title */ - -#kc-header { - position: fixed; - inset: 0 0 auto 0; - z-index: 10; - height: var(--mancala-bar-height); - margin: 0; - padding: 0 4vw; - display: flex; - align-items: center; - background: var(--mancala-bar); -} - -#kc-header-wrapper { - display: flex; - align-items: center; - gap: 10px; - margin: 0; - padding: 0; - font-family: var(--mancala-font); - font-size: 2em; - font-weight: bold; - line-height: 1; - letter-spacing: normal; - text-transform: none; - text-align: start; - color: var(--mancala-bar-text) !important; -} - -#kc-header-wrapper::before { - content: ""; - flex: none; - width: 30px; - height: 30px; - background: url("../img/logo.svg") center / contain no-repeat; -} - -.pf-v5-c-login { - min-height: 100vh; - padding-block-start: calc(var(--mancala-bar-height) + 2rem); - padding-block-end: 2rem; -} - -.pf-v5-c-login__container { - max-width: calc(30rem + 32px); - padding-inline: 16px; - grid-template-columns: minmax(0, 30rem); -} - -/* Card: the board, with a small board at the start of a game on top */ - -.pf-v5-c-login__main { - --pf-v5-c-login__main--BackgroundColor: var(--mancala-board); - --pf-v5-c-login__main--BoxShadow: 0 8px 24px rgba(0, 0, 0, 0.18); - --pf-v5-c-login__main-footer-band--BorderTopColor: var(--mancala-divider); - padding-block-start: 2rem; - border-radius: 24px; - color: var(--mancala-ink); -} - -.pf-v5-c-login__main::before { - content: ""; - display: block; - width: min(calc(100% - 4rem), 260px); - aspect-ratio: 312 / 76; - margin: 0 auto; - background: var(--mancala-board-image) center / contain no-repeat; -} - -.pf-v5-c-login__main-header { - --pf-v5-c-login__main-header--PaddingTop: 1.5rem; - border-top: none; -} - -#kc-page-title { - color: var(--mancala-ink); - font-family: var(--mancala-font); - font-size: 1.5rem; - font-weight: bold; -} - -.pf-v5-c-login__main hr { - border-color: var(--mancala-divider); -} - -.pf-v5-c-login__main a:not(.pf-v5-c-button) { - color: var(--mancala-ink); - text-decoration: underline; -} - -.pf-v5-c-login__main a:not(.pf-v5-c-button):hover { - color: var(--mancala-ink-strong); -} - -/* Inputs: pits */ - -.pf-v5-c-form-control { - --pf-v5-c-form-control--BackgroundColor: var(--mancala-pit); - --pf-v5-c-form-control--Color: var(--mancala-ink); - --pf-v5-c-form-control--m-placeholder--Color: var(--mancala-ink-soft); - --pf-v5-c-form-control--m-readonly--BackgroundColor: var(--mancala-pit-readonly); - --pf-v5-c-form-control__toggle-icon--Color: var(--mancala-ink); - --pf-v5-c-form-control--PaddingLeft: 1rem; - --pf-v5-c-form-control--PaddingRight: 1rem; - border: 1px solid var(--mancala-pit-edge); - border-radius: 999px; - box-shadow: inset 0 2px 5px var(--mancala-pit-shadow); - font-family: var(--mancala-font); -} - -.pf-v5-c-form-control:has(textarea) { - border-radius: 16px; -} - -.pf-v5-c-form-control::before, -.pf-v5-c-form-control::after { - content: none; -} - -.pf-v5-c-form-control > :is(input, select, textarea) { - font-family: inherit; - background-color: transparent; - border-radius: inherit; -} - -.pf-v5-c-form-control > select { - padding-inline-start: 1rem; -} - -.pf-v5-c-form-control > :is(input, select, textarea):focus { - outline: none; -} - -.pf-v5-c-form-control:hover { - border-color: var(--mancala-ink); -} - -.pf-v5-c-form-control:focus-within { - border-color: var(--mancala-ink); - outline: 2px solid var(--mancala-ink); - outline-offset: 2px; -} - -.pf-v5-c-form-control.pf-m-error { - border-color: var(--mancala-danger); - box-shadow: inset 0 2px 5px var(--mancala-pit-shadow), 0 0 0 1px var(--mancala-danger); -} - -.pf-v5-c-login__main .pf-v5-c-helper-text__item.pf-m-error, -.pf-v5-c-login__main .pf-v5-c-helper-text__item.pf-m-error .pf-v5-c-helper-text__item-text, -.pf-v5-c-login__main .pf-v5-c-helper-text__item-text.pf-m-error { - color: var(--mancala-danger); -} - -/* Password field and its show button form one pill */ - -.pf-v5-c-input-group > .pf-v5-c-input-group__item:first-child:not(:last-child) .pf-v5-c-form-control { - border-start-end-radius: 0; - border-end-end-radius: 0; -} - -.pf-v5-c-input-group > .pf-v5-c-input-group__item:last-child:not(:first-child) .pf-v5-c-button { - border-radius: 0 999px 999px 0; -} - -/* Buttons: pill shaped like the game's */ - -.pf-v5-c-button { - --pf-v5-c-button--BorderRadius: 999px; - --pf-v5-c-button--after--BorderRadius: 999px; - - --pf-v5-c-button--m-primary--BackgroundColor: var(--mancala-stone); - --pf-v5-c-button--m-primary--hover--BackgroundColor: var(--mancala-stone-hover); - --pf-v5-c-button--m-primary--focus--BackgroundColor: var(--mancala-stone-hover); - --pf-v5-c-button--m-primary--active--BackgroundColor: var(--mancala-stone-hover); - --pf-v5-c-button--m-primary--Color: var(--mancala-stone-text); - --pf-v5-c-button--m-primary--hover--Color: var(--mancala-stone-text); - --pf-v5-c-button--m-primary--focus--Color: var(--mancala-stone-text); - --pf-v5-c-button--m-primary--active--Color: var(--mancala-stone-text); - - --pf-v5-c-button--m-secondary--Color: var(--mancala-ink); - --pf-v5-c-button--m-secondary--hover--Color: var(--mancala-ink); - --pf-v5-c-button--m-secondary--focus--Color: var(--mancala-ink); - --pf-v5-c-button--m-secondary--active--Color: var(--mancala-ink); - --pf-v5-c-button--m-secondary--after--BorderColor: var(--mancala-ink); - --pf-v5-c-button--m-secondary--hover--after--BorderColor: var(--mancala-ink); - --pf-v5-c-button--m-secondary--focus--after--BorderColor: var(--mancala-ink); - --pf-v5-c-button--m-secondary--active--after--BorderColor: var(--mancala-ink); - --pf-v5-c-button--m-secondary--hover--BackgroundColor: var(--mancala-hover); - --pf-v5-c-button--m-secondary--focus--BackgroundColor: var(--mancala-hover); - --pf-v5-c-button--m-secondary--active--BackgroundColor: var(--mancala-hover); - - --pf-v5-c-button--m-link--Color: var(--mancala-ink); - --pf-v5-c-button--m-link--hover--Color: var(--mancala-ink-strong); - --pf-v5-c-button--m-link--focus--Color: var(--mancala-ink-strong); - --pf-v5-c-button--m-link--active--Color: var(--mancala-ink-strong); - - --pf-v5-c-button--m-control--BackgroundColor: var(--mancala-pit); - --pf-v5-c-button--m-control--hover--BackgroundColor: var(--mancala-pit); - --pf-v5-c-button--m-control--focus--BackgroundColor: var(--mancala-pit); - --pf-v5-c-button--m-control--active--BackgroundColor: var(--mancala-pit); - --pf-v5-c-button--m-control--Color: var(--mancala-ink); - --pf-v5-c-button--m-control--hover--Color: var(--mancala-ink-strong); - --pf-v5-c-button--m-control--focus--Color: var(--mancala-ink-strong); - --pf-v5-c-button--m-control--active--Color: var(--mancala-ink-strong); - font-family: var(--mancala-font); -} - -.pf-v5-c-button.pf-m-primary { - font-weight: bold; -} - -.pf-v5-c-button.pf-m-control::after { - border: 1px solid var(--mancala-pit-edge); - border-radius: inherit; -} - -.pf-v5-c-button.pf-m-control:hover::after { - border-color: var(--mancala-ink); -} - -.pf-v5-c-button:focus-visible { - outline: 2px solid var(--mancala-ink); - outline-offset: 2px; -} - -.pf-v5-c-check__input { - accent-color: var(--mancala-stone); -} diff --git a/apps/keycloak/themes/mancala/login/resources/img/board-dark.svg b/apps/keycloak/themes/mancala/login/resources/img/board-dark.svg deleted file mode 100644 index c1edcbd..0000000 --- a/apps/keycloak/themes/mancala/login/resources/img/board-dark.svg +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/keycloak/themes/mancala/login/resources/img/board-mask.svg b/apps/keycloak/themes/mancala/login/resources/img/board-mask.svg new file mode 100644 index 0000000..f2c7ee1 --- /dev/null +++ b/apps/keycloak/themes/mancala/login/resources/img/board-mask.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/keycloak/themes/mancala/login/resources/img/board.svg b/apps/keycloak/themes/mancala/login/resources/img/board.svg deleted file mode 100644 index 2fad955..0000000 --- a/apps/keycloak/themes/mancala/login/resources/img/board.svg +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/keycloak/themes/mancala/login/resources/img/logo-mask.svg b/apps/keycloak/themes/mancala/login/resources/img/logo-mask.svg new file mode 100644 index 0000000..d704c49 --- /dev/null +++ b/apps/keycloak/themes/mancala/login/resources/img/logo-mask.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + diff --git a/apps/keycloak/themes/mancala/login/resources/img/logo.svg b/apps/keycloak/themes/mancala/login/resources/img/logo.svg deleted file mode 100644 index cf65dfd..0000000 --- a/apps/keycloak/themes/mancala/login/resources/img/logo.svg +++ /dev/null @@ -1,76 +0,0 @@ - - - - - - - - - - - - - - diff --git a/apps/keycloak/themes/mancala/login/resources/img/stones-mask.svg b/apps/keycloak/themes/mancala/login/resources/img/stones-mask.svg new file mode 100644 index 0000000..2ba2360 --- /dev/null +++ b/apps/keycloak/themes/mancala/login/resources/img/stones-mask.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/keycloak/themes/mancala/login/resources/js/theme.js b/apps/keycloak/themes/mancala/login/resources/js/theme.js new file mode 100644 index 0000000..813a140 --- /dev/null +++ b/apps/keycloak/themes/mancala/login/resources/js/theme.js @@ -0,0 +1,13 @@ +// Puts the web app's theme on the page: the one the player chose, which the web app keeps in the mancala_theme +// cookie, or else Ceviz for a dark system color scheme and Yeşim for a light one. Runs in , before the page +// is drawn. The dark themes also get PatternFly's dark mode. +(function () { + var DARK_THEMES = { ceviz: true, gece: true }; + var THEMES = { ceviz: true, gece: true, yesim: true }; + var match = document.cookie.match(/(?:^|;\s*)mancala_theme=([a-z]+)/); + var theme = match && THEMES[match[1]] ? match[1] + : window.matchMedia("(prefers-color-scheme: dark)").matches ? "ceviz" : "yesim"; + var classList = document.documentElement.classList; + classList.add("t-" + theme); + if (DARK_THEMES[theme]) classList.add("pf-v5-theme-dark"); +})(); diff --git a/apps/keycloak/themes/mancala/login/theme.properties b/apps/keycloak/themes/mancala/login/theme.properties index ec25cc5..40328f8 100644 --- a/apps/keycloak/themes/mancala/login/theme.properties +++ b/apps/keycloak/themes/mancala/login/theme.properties @@ -1,6 +1,11 @@ -# Login pages in the look of the game. Keeps keycloak.v2's templates and only adds a stylesheet, -# so Keycloak upgrades do not need template changes. +# Login pages in the look of the web app, in its three themes (docs/specs/theme-polish). Keeps keycloak.v2's +# templates and only adds a stylesheet and a script, so Keycloak upgrades do not need template changes. parent=keycloak.v2 import=common/keycloak -styles=css/styles.css css/mancala.css +# Browsers cache theme resources under a path that only changes with Keycloak's version: give a stylesheet a new name +# when old copies must not be used (mancala.css was the grey theme's). +styles=css/styles.css css/login.css +# Picks the theme before the page is drawn; it also sets PatternFly's dark mode, so Keycloak's own is off. +scripts=js/theme.js +darkMode=false diff --git a/docs/specs/theme-polish/design.md b/docs/specs/theme-polish/design.md new file mode 100644 index 0000000..ad5bfde --- /dev/null +++ b/docs/specs/theme-polish/design.md @@ -0,0 +1,49 @@ +# Theme polish: design + +See `requirements.md`; requirement ids (R1.1, …) are referenced below. + +## Decisions + +| Decision | Reason | +|---|---| +| The web app writes the theme's id to a `mancala_theme` cookie; a small script in the login theme reads it and puts the theme's class on `` | Keycloak's pages are on another origin (`localhost:8080`, `auth.playmancala.app`), so they cannot read the app's `localStorage`. Cookies are shared: host-only cookies across the ports of `localhost`, and a cookie for `playmancala.app` with its subdomains (R1.2). | +| The script runs in ``, before the page is drawn | No flash of another theme. | +| Without the cookie, the script picks Ceviz or Yeşim from `prefers-color-scheme` (R1.3) | The app's default theme is Ceviz (dark); Yeşim is its only light theme. | +| `darkMode=false` in the theme; the script adds PatternFly's `pf-v5-theme-dark` itself for the dark themes | Keycloak's own dark mode follows the system and would fight the chosen theme. | +| The theme stays a stylesheet on top of `keycloak.v2`, now with the three themes' tokens copied from `apps/frontend/src/styles/app.css` | Templates stay Keycloak's, so upgrades need no template changes. | +| The logo and the small board on the card are masks filled with the theme's colors | One image each for all three themes. | +| The app's name comes from CSS by the page's language | The realm's display name is one string; the header shows "Köçürme" for Turkish and "Mancala" otherwise, like the web app. | +| Notyf stays for messages, configured for the top centre and restyled with the theme's variables (R2) | It is used from plain functions (`Util.checkConnectionAndMaybeAlert`), not only components; a React toast would need a context everywhere. | + +## Login theme (`apps/keycloak/themes/mancala/login`) + +- `theme.properties`: `darkMode=false`, `scripts=js/theme.js`, styles `css/styles.css css/login.css`. +- `js/theme.js`: reads `mancala_theme` (`ceviz`, `gece`, `yesim`), falls back to the color scheme, adds `t-` and, + for Ceviz and Gece, `pf-v5-theme-dark` to ``. +- `css/login.css`: the Bricolage Grotesque font; the tokens of `.t-ceviz`, `.t-gece`, `.t-yesim`; PatternFly's + variables mapped to them; the header like the app's (logo, name), the card on `--surface`, inputs on `--surface2`, + primary buttons in `--accent`, secondary ones outlined, links in `--ink`, errors in `--live`. +- `img/logo-mask.svg`, `img/board-mask.svg`: alpha masks. + +## Web app + +### Header (R3) + +- `AppHeader`: logo, the page's actions (children), how to play, `SettingsMenu`, `AccountMenu`. The signed-in user + comes from an `AuthUserContext` that `MancalaApp` provides, so pages need not pass it down. +- `SettingsMenu` (new) replaces `ThemeSwatches`: a `@szhsin/react-menu` menu like the account menu, with a "Theme" + header and one checkbox item per theme (swatch, name). Checked items in every menu (the theme, the last bot level) + show a check at their end instead of the library's mark in front. +- Icon buttons in the header use `.ibtn` (round, no outline, a surface on hover and while open); the account button + shows `getInitials` in an accent circle. +- Home: the header's actions get `hide-m`; a `.qa.show-m` row at the top of the page holds New Game and the bot menu + (`BotLevelMenu` with a full button) below 640 px. + +### Messages and the theme cookie + +- `theme/themes.ts`: `saveThemeCookie(theme)`, called with `applyThemeClass` at start and on every theme change. The + cookie lasts a year, `SameSite=Lax`, `Secure` on https, and on a real domain is set for the last two labels of the + host (`playmancala.app`). +- `util/Notyf.ts`: `position: { x: "center", y: "top" }`, a shorter duration, no icons, dismissible by click. +- `styles/app.css`: Notyf's container below the header; the toast in `--ink` on `--bg` (an inverted chip, readable on + every theme), the app's font, rounded. diff --git a/docs/specs/theme-polish/requirements.md b/docs/specs/theme-polish/requirements.md new file mode 100644 index 0000000..8f6cede --- /dev/null +++ b/docs/specs/theme-polish/requirements.md @@ -0,0 +1,39 @@ +# Theme polish: requirements + +Status: implemented on branch `feature/theme-polish` (based on `develop`); checked in Chrome against the local stack. +Scope: the Keycloak login theme, the web app's short messages and its header. Follows `web-redesign`. + +## Goals + +The login pages and the web app's messages look like the rest of the app in each of its three themes (Ceviz, Gece, +Yeşim), and messages appear where they are noticed without covering the game. + +## Requirements + +### R1. Login pages +- R1.1 The login, registration, password reset and other Keycloak login pages use the web redesign's look: its colors, + font, logo, rounded cards, pill buttons and the app's name ("Mancala", in Turkish "Köçürme"). +- R1.2 They use the theme the player chose in the web app on the same device, also right after changing it. +- R1.3 Without a chosen theme (e.g. a first visit straight to the login page), they follow the system's color scheme: + Ceviz when it is dark, Yeşim when it is light. +- R1.4 Errors, info messages and the language select stay readable in every theme; the pages work at phone width + without horizontal scrolling. + +### R2. Messages in the web app +- R2.1 Short messages (e.g. "Opponent's turn", "Connection lost") use the theme's colors and font. +- R2.2 They appear at the top centre, just below the header, not in the bottom right corner. +- R2.3 Several messages stack without covering the header; each goes away by itself or when clicked. + +### R3. Header +- R3.1 Every page's header has, in this order: the logo and name, the page's actions, how to play, settings and the + account (the design canvas's updated header). +- R3.2 How to play, settings and the account are round icon buttons; the account shows the player's initials. +- R3.3 Settings opens a menu with the three themes, each with its swatch and name, the chosen one checked. The theme + swatches are no longer in the header. +- R3.4 The account menu (log in, sign up, log out) is on every page, not only the home page. +- R3.5 At phone width the home page's actions (New Game, Play the bot) leave the header for a row of two buttons at the + top of the page; the game page keeps its action as an icon. + +## Out of scope (TODO) +- The design's "Sound effects" switch in settings: the app has no sounds yet. +- Keycloak's email theme (password reset emails). diff --git a/docs/specs/theme-polish/tasks.md b/docs/specs/theme-polish/tasks.md new file mode 100644 index 0000000..ed1222c --- /dev/null +++ b/docs/specs/theme-polish/tasks.md @@ -0,0 +1,12 @@ +# Theme polish: tasks + +Work through the tasks in order; each ends with its checks passing and a commit. Tick a task when it is committed. + +- [x] 1. **Spec**: requirements, design and tasks. +- [x] 2. **Web app messages**: Notyf at the top centre in the theme's colors. +- [x] 3. **Theme cookie**: the web app saves the chosen theme for the login pages. +- [x] 4. **Login theme**: the script, the three themes' styles, logo and board masks; checked in Chrome in all three + themes, at desktop and phone width, with and without the cookie. +- [x] 5. **Docs**: `AGENTS.md` (specs list, the cookie), `apps/keycloak/README.md`, `TODO.md`. +- [x] 6. **Header**: the new order, the settings menu with the themes, the account on every page, the home page's + actions on phones; checked in Chrome in all three themes at desktop and phone width. diff --git a/packages/core/src/localization/en.ts b/packages/core/src/localization/en.ts index 36c277c..c5969bd 100644 --- a/packages/core/src/localization/en.ts +++ b/packages/core/src/localization/en.ts @@ -194,4 +194,10 @@ export const en = { MovedFirst: "Moved first", EndedOnTime: "time", EndedByLeaving: "left", + Settings: "Settings", + Theme: "Theme", + ThemeWalnutName: "Walnut", + ThemeNightName: "Night", + ThemeJadeName: "Jade", + PlayBot: "Play the bot", } \ No newline at end of file diff --git a/packages/core/src/localization/tr.ts b/packages/core/src/localization/tr.ts index 83d6bdc..17f7895 100644 --- a/packages/core/src/localization/tr.ts +++ b/packages/core/src/localization/tr.ts @@ -194,4 +194,10 @@ export const tr = { MovedFirst: "İlk hamle", EndedOnTime: "süre", EndedByLeaving: "ayrıldı", + Settings: "Ayarlar", + Theme: "Tema", + ThemeWalnutName: "Ceviz", + ThemeNightName: "Gece", + ThemeJadeName: "Yeşim", + PlayBot: "Bota karşı", }; \ No newline at end of file