From 35c2f904a0a8e55e41b64f85ddb8f942c4a35a8c Mon Sep 17 00:00:00 2001 From: Halit Aksoy Date: Sat, 17 Oct 2026 02:28:40 +0300 Subject: [PATCH 1/8] docs(theme-polish): add the spec The login pages in the app's three themes, chosen through a cookie, and the web app's messages in the theme's colors at the top centre. --- docs/specs/theme-polish/design.md | 35 +++++++++++++++++++++++++ docs/specs/theme-polish/requirements.md | 28 ++++++++++++++++++++ docs/specs/theme-polish/tasks.md | 10 +++++++ 3 files changed, 73 insertions(+) create mode 100644 docs/specs/theme-polish/design.md create mode 100644 docs/specs/theme-polish/requirements.md create mode 100644 docs/specs/theme-polish/tasks.md diff --git a/docs/specs/theme-polish/design.md b/docs/specs/theme-polish/design.md new file mode 100644 index 0000000..111bede --- /dev/null +++ b/docs/specs/theme-polish/design.md @@ -0,0 +1,35 @@ +# 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/mancala.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/mancala.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 + +- `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..9b637c1 --- /dev/null +++ b/docs/specs/theme-polish/requirements.md @@ -0,0 +1,28 @@ +# Theme polish: requirements + +Status: in progress, on branch `feature/theme-polish` (based on `develop`). +Scope: the Keycloak login theme and the web app's short messages. 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. + +## Out of scope (TODO) +- 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..2c6c9e7 --- /dev/null +++ b/docs/specs/theme-polish/tasks.md @@ -0,0 +1,10 @@ +# 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. +- [ ] 2. **Web app messages**: Notyf at the top centre in the theme's colors. +- [ ] 3. **Theme cookie**: the web app saves the chosen theme for the login pages. +- [ ] 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. +- [ ] 5. **Docs**: `AGENTS.md` (specs list, the cookie), `apps/keycloak/README.md`, `TODO.md`. -- 2.45.2 From dfdb4b057c69408c72d615a72729daa7afd508b3 Mon Sep 17 00:00:00 2001 From: Halit Aksoy Date: Sat, 17 Oct 2026 02:39:41 +0300 Subject: [PATCH 2/8] feat(frontend): show messages at the top in the theme's colors Messages appear at the top centre below the header as an inverted chip of the theme, in the app's font and without icons, and close on a click instead of sliding in at the bottom right in Notyf's red. --- apps/frontend/src/styles/app.css | 25 +++++++++++++++++++++++++ apps/frontend/src/util/Notyf.ts | 24 ++++++++++++++++++++---- docs/specs/theme-polish/tasks.md | 2 +- 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/apps/frontend/src/styles/app.css b/apps/frontend/src/styles/app.css index 99c6653..b5daa83 100644 --- a/apps/frontend/src/styles/app.css +++ b/apps/frontend/src/styles/app.css @@ -242,3 +242,28 @@ a { color: inherit; } @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/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/docs/specs/theme-polish/tasks.md b/docs/specs/theme-polish/tasks.md index 2c6c9e7..cf4b511 100644 --- a/docs/specs/theme-polish/tasks.md +++ b/docs/specs/theme-polish/tasks.md @@ -3,7 +3,7 @@ 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. -- [ ] 2. **Web app messages**: Notyf at the top centre in the theme's colors. +- [x] 2. **Web app messages**: Notyf at the top centre in the theme's colors. - [ ] 3. **Theme cookie**: the web app saves the chosen theme for the login pages. - [ ] 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. -- 2.45.2 From 64149320acc6fd7db4aedf3de5f69cb72a13f442 Mon Sep 17 00:00:00 2001 From: Halit Aksoy Date: Sat, 17 Oct 2026 02:40:07 +0300 Subject: [PATCH 3/8] feat(frontend): remember the theme in a cookie for the login pages Keycloak's pages are on another origin; a mancala_theme cookie reaches them on localhost's ports and, set for playmancala.app, on its auth subdomain. --- apps/frontend/src/theme/themes.ts | 16 ++++++++++++++++ docs/specs/theme-polish/tasks.md | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/apps/frontend/src/theme/themes.ts b/apps/frontend/src/theme/themes.ts index 09baab5..30d118e 100644 --- a/apps/frontend/src/theme/themes.ts +++ b/apps/frontend/src/theme/themes.ts @@ -87,8 +87,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/docs/specs/theme-polish/tasks.md b/docs/specs/theme-polish/tasks.md index cf4b511..5ae6e72 100644 --- a/docs/specs/theme-polish/tasks.md +++ b/docs/specs/theme-polish/tasks.md @@ -4,7 +4,7 @@ Work through the tasks in order; each ends with its checks passing and a commit. - [x] 1. **Spec**: requirements, design and tasks. - [x] 2. **Web app messages**: Notyf at the top centre in the theme's colors. -- [ ] 3. **Theme cookie**: the web app saves the chosen theme for the login pages. +- [x] 3. **Theme cookie**: the web app saves the chosen theme for the login pages. - [ ] 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. - [ ] 5. **Docs**: `AGENTS.md` (specs list, the cookie), `apps/keycloak/README.md`, `TODO.md`. -- 2.45.2 From 342a2455fab1814d70867b19ae4ae3c2955a0a3e Mon Sep 17 00:00:00 2001 From: Halit Aksoy Date: Sat, 17 Oct 2026 02:44:54 +0300 Subject: [PATCH 4/8] feat(keycloak): give the login pages the app's three themes A script in the page's head reads the theme the web app saved in the mancala_theme cookie, or follows the system's color scheme, and puts the theme's class on the page. The stylesheet takes the redesign's colors, font, header, cards, fields and pill buttons; the logo and the board on the card are masks in the theme's colors. --- .../mancala/login/resources/css/mancala.css | 440 +++++++++++------- .../login/resources/img/board-dark.svg | 26 -- .../login/resources/img/board-mask.svg | 24 + .../mancala/login/resources/img/board.svg | 26 -- .../mancala/login/resources/img/logo-mask.svg | 13 + .../mancala/login/resources/img/logo.svg | 76 --- .../login/resources/img/stones-mask.svg | 51 ++ .../mancala/login/resources/js/theme.js | 13 + .../themes/mancala/login/theme.properties | 7 +- docs/specs/theme-polish/tasks.md | 2 +- 10 files changed, 390 insertions(+), 288 deletions(-) delete mode 100644 apps/keycloak/themes/mancala/login/resources/img/board-dark.svg create mode 100644 apps/keycloak/themes/mancala/login/resources/img/board-mask.svg delete mode 100644 apps/keycloak/themes/mancala/login/resources/img/board.svg create mode 100644 apps/keycloak/themes/mancala/login/resources/img/logo-mask.svg delete mode 100644 apps/keycloak/themes/mancala/login/resources/img/logo.svg create mode 100644 apps/keycloak/themes/mancala/login/resources/img/stones-mask.svg create mode 100644 apps/keycloak/themes/mancala/login/resources/js/theme.js diff --git a/apps/keycloak/themes/mancala/login/resources/css/mancala.css b/apps/keycloak/themes/mancala/login/resources/css/mancala.css index 86d0b7d..eabc7d0 100644 --- a/apps/keycloak/themes/mancala/login/resources/css/mancala.css +++ b/apps/keycloak/themes/mancala/login/resources/css/mancala.css @@ -1,108 +1,111 @@ /* - * 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. + * 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. */ -: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; +@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"); - --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); +.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; } -: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"); +.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(--mancala-bg); - font-family: var(--mancala-font); + background: var(--bg); + color: var(--ink); + font-family: var(--font); } -/* Header: the game's app bar with its logo and title */ +/* Header: the app's header with its logo and name */ #kc-header { position: fixed; inset: 0 0 auto 0; z-index: 10; - height: var(--mancala-bar-height); + height: var(--bar-height); margin: 0; - padding: 0 4vw; + padding: 0 32px; + box-sizing: border-box; display: flex; align-items: center; - background: var(--mancala-bar); + 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-family: var(--mancala-font); - font-size: 2em; - font-weight: bold; - line-height: 1; + font-size: 0; letter-spacing: normal; text-transform: none; - text-align: start; - color: var(--mancala-bar-text) !important; + color: var(--ink) !important; } #kc-header-wrapper::before { content: ""; flex: none; - width: 30px; - height: 30px; - background: url("../img/logo.svg") center / contain no-repeat; + 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(--mancala-bar-height) + 2rem); + padding-block-start: calc(var(--bar-height) + 2rem); padding-block-end: 2rem; } @@ -112,24 +115,42 @@ grid-template-columns: minmax(0, 30rem); } -/* Card: the board, with a small board at the start of a game on top */ +/* 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(--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); + --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: 24px; - color: var(--mancala-ink); + 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 { - 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; + 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 { @@ -138,43 +159,56 @@ } #kc-page-title { - color: var(--mancala-ink); - font-family: var(--mancala-font); - font-size: 1.5rem; - font-weight: bold; + 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(--mancala-divider); + border-color: var(--line); } .pf-v5-c-login__main a:not(.pf-v5-c-button) { - color: var(--mancala-ink); + 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(--mancala-ink-strong); + color: var(--accent); + text-decoration-color: currentColor; } -/* Inputs: pits */ +.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(--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--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; - 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; + min-height: 46px; + border: 1.5px solid var(--line); + border-radius: 14px; + font-family: var(--font); } .pf-v5-c-form-control::before, @@ -184,6 +218,7 @@ .pf-v5-c-form-control > :is(input, select, textarea) { font-family: inherit; + color: var(--ink); background-color: transparent; border-radius: inherit; } @@ -192,103 +227,194 @@ 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(--mancala-ink); + border-color: var(--muted); } .pf-v5-c-form-control:focus-within { - border-color: var(--mancala-ink); - outline: 2px solid var(--mancala-ink); - outline-offset: 2px; + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accentSoft); } .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); + 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(--mancala-danger); + color: var(--live); + font-weight: 700; } -/* Password field and its show button form one pill */ +/* The language select in the card's header */ -.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-login__main-header-utilities .pf-v5-c-form-control { + min-height: 40px; + border-radius: 20px; } -.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; +/* 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; } -/* Buttons: pill shaped like the game's */ +.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: 999px; - --pf-v5-c-button--after--BorderRadius: 999px; + --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(--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-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--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-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(--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-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(--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--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 { - font-weight: bold; + min-height: 46px; + font-size: 15px; } -.pf-v5-c-button.pf-m-control::after { - border: 1px solid var(--mancala-pit-edge); - border-radius: inherit; +.pf-v5-c-button.pf-m-primary:hover { + filter: brightness(1.06); } -.pf-v5-c-button.pf-m-control:hover::after { - border-color: var(--mancala-ink); +/* 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: 2px solid var(--mancala-ink); - outline-offset: 2px; + outline: 3px solid var(--accent); + outline-offset: 3px; } .pf-v5-c-check__input { - accent-color: var(--mancala-stone); + 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/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..8fe0280 100644 --- a/apps/keycloak/themes/mancala/login/theme.properties +++ b/apps/keycloak/themes/mancala/login/theme.properties @@ -1,6 +1,9 @@ -# 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 +# 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/tasks.md b/docs/specs/theme-polish/tasks.md index 5ae6e72..c92a562 100644 --- a/docs/specs/theme-polish/tasks.md +++ b/docs/specs/theme-polish/tasks.md @@ -5,6 +5,6 @@ Work through the tasks in order; each ends with its checks passing and a commit. - [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. -- [ ] 4. **Login theme**: the script, the three themes' styles, logo and board masks; checked in Chrome in all three +- [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. - [ ] 5. **Docs**: `AGENTS.md` (specs list, the cookie), `apps/keycloak/README.md`, `TODO.md`. -- 2.45.2 From 537a874374f858b3ae85c0add567de7cc33f37bc Mon Sep 17 00:00:00 2001 From: Halit Aksoy Date: Sat, 17 Oct 2026 02:45:11 +0300 Subject: [PATCH 5/8] docs: describe the themed login pages and messages --- AGENTS.md | 12 +++++++----- TODO.md | 1 - apps/keycloak/README.md | 17 ++++++++++++----- docs/specs/theme-polish/requirements.md | 2 +- docs/specs/theme-polish/tasks.md | 2 +- 5 files changed, 21 insertions(+), 13 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 1792ff0..c340787 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,11 @@ 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. 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..d75042a 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 diff --git a/apps/keycloak/README.md b/apps/keycloak/README.md index e96e678..607af16 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/mancala.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/docs/specs/theme-polish/requirements.md b/docs/specs/theme-polish/requirements.md index 9b637c1..d7e312e 100644 --- a/docs/specs/theme-polish/requirements.md +++ b/docs/specs/theme-polish/requirements.md @@ -1,6 +1,6 @@ # Theme polish: requirements -Status: in progress, on branch `feature/theme-polish` (based on `develop`). +Status: implemented on branch `feature/theme-polish` (based on `develop`); checked in Chrome against the local stack. Scope: the Keycloak login theme and the web app's short messages. Follows `web-redesign`. ## Goals diff --git a/docs/specs/theme-polish/tasks.md b/docs/specs/theme-polish/tasks.md index c92a562..d6ce749 100644 --- a/docs/specs/theme-polish/tasks.md +++ b/docs/specs/theme-polish/tasks.md @@ -7,4 +7,4 @@ Work through the tasks in order; each ends with its checks passing and a commit. - [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. -- [ ] 5. **Docs**: `AGENTS.md` (specs list, the cookie), `apps/keycloak/README.md`, `TODO.md`. +- [x] 5. **Docs**: `AGENTS.md` (specs list, the cookie), `apps/keycloak/README.md`, `TODO.md`. -- 2.45.2 From 6f88658c60147a427da3783b89702554c5299f61 Mon Sep 17 00:00:00 2001 From: Halit Aksoy Date: Sat, 17 Oct 2026 02:59:26 +0300 Subject: [PATCH 6/8] feat(frontend): simplify the header as in the updated design The header is now the logo, the page's actions, how to play, settings and the account. The theme swatches move into a settings menu; the account shows the player's initials and is on every page through an AuthUserContext. On phones the home page's actions leave the header for a row of two buttons. Checked menu items get a check at their end. --- TODO.md | 1 + apps/frontend/src/MancalaApp.tsx | 5 +- apps/frontend/src/components/BotLevelMenu.tsx | 8 +- apps/frontend/src/components/Icons.tsx | 8 +- .../src/components/headerbar/AccountMenu.tsx | 11 +-- .../src/components/headerbar/AppHeader.tsx | 29 ++++--- .../src/components/headerbar/SettingsMenu.tsx | 37 +++++++++ .../components/headerbar/ThemeSwatches.tsx | 26 ------ apps/frontend/src/context/AuthUserContext.ts | 5 ++ apps/frontend/src/routes/Home.tsx | 13 ++- apps/frontend/src/styles/app.css | 80 ++++++++++++++----- apps/frontend/src/theme/themes.ts | 5 ++ docs/specs/theme-polish/design.md | 14 ++++ docs/specs/theme-polish/requirements.md | 13 ++- docs/specs/theme-polish/tasks.md | 2 + packages/core/src/localization/en.ts | 6 ++ packages/core/src/localization/tr.ts | 6 ++ 17 files changed, 191 insertions(+), 78 deletions(-) create mode 100644 apps/frontend/src/components/headerbar/SettingsMenu.tsx delete mode 100644 apps/frontend/src/components/headerbar/ThemeSwatches.tsx create mode 100644 apps/frontend/src/context/AuthUserContext.ts diff --git a/TODO.md b/TODO.md index d75042a..89cc2e9 100644 --- a/TODO.md +++ b/TODO.md @@ -79,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 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,13 @@ 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; } } diff --git a/apps/frontend/src/theme/themes.ts b/apps/frontend/src/theme/themes.ts index 30d118e..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", diff --git a/docs/specs/theme-polish/design.md b/docs/specs/theme-polish/design.md index 111bede..05a02d2 100644 --- a/docs/specs/theme-polish/design.md +++ b/docs/specs/theme-polish/design.md @@ -27,6 +27,20 @@ See `requirements.md`; requirement ids (R1.1, …) are referenced below. ## 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`). diff --git a/docs/specs/theme-polish/requirements.md b/docs/specs/theme-polish/requirements.md index d7e312e..8f6cede 100644 --- a/docs/specs/theme-polish/requirements.md +++ b/docs/specs/theme-polish/requirements.md @@ -1,7 +1,7 @@ # 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 and the web app's short messages. Follows `web-redesign`. +Scope: the Keycloak login theme, the web app's short messages and its header. Follows `web-redesign`. ## Goals @@ -24,5 +24,16 @@ Yeşim), and messages appear where they are noticed without covering the game. - 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 index d6ce749..ed1222c 100644 --- a/docs/specs/theme-polish/tasks.md +++ b/docs/specs/theme-polish/tasks.md @@ -8,3 +8,5 @@ Work through the tasks in order; each ends with its checks passing and a commit. - [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 -- 2.45.2 From 077cf0185c33e7e29d1851c0dbf43355e0b31ed2 Mon Sep 17 00:00:00 2001 From: Halit Aksoy Date: Sat, 17 Oct 2026 03:01:25 +0300 Subject: [PATCH 7/8] fix(keycloak): stop browsers from keeping the old login look Theme files are cached under a path that only changes with Keycloak's version, for 30 days, so returning players kept the grey stylesheet. It is renamed to login.css, and theme files are now cached for an hour. --- AGENTS.md | 4 +++- apps/keycloak/Dockerfile | 3 +++ apps/keycloak/README.md | 2 +- .../mancala/login/resources/css/{mancala.css => login.css} | 0 apps/keycloak/themes/mancala/login/theme.properties | 4 +++- docs/specs/theme-polish/design.md | 4 ++-- 6 files changed, 12 insertions(+), 5 deletions(-) rename apps/keycloak/themes/mancala/login/resources/css/{mancala.css => login.css} (100%) diff --git a/AGENTS.md b/AGENTS.md index c340787..f5f5215 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -144,7 +144,9 @@ which helps to check what the server holds. - Keycloak sets `Secure` cookies; browsers send them to `http://localhost`, simple HTTP clients do not. - 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. Keycloak in `start` mode caches themes and keeps gzipped + 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. 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 607af16..39dc52e 100644 --- a/apps/keycloak/README.md +++ b/apps/keycloak/README.md @@ -38,7 +38,7 @@ console too, or delete the realm and restart Keycloak. `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/mancala.css` and `resources/js/theme.js`, so its templates and behaviour stay Keycloak's own and +`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 diff --git a/apps/keycloak/themes/mancala/login/resources/css/mancala.css b/apps/keycloak/themes/mancala/login/resources/css/login.css similarity index 100% rename from apps/keycloak/themes/mancala/login/resources/css/mancala.css rename to apps/keycloak/themes/mancala/login/resources/css/login.css diff --git a/apps/keycloak/themes/mancala/login/theme.properties b/apps/keycloak/themes/mancala/login/theme.properties index 8fe0280..40328f8 100644 --- a/apps/keycloak/themes/mancala/login/theme.properties +++ b/apps/keycloak/themes/mancala/login/theme.properties @@ -3,7 +3,9 @@ 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 index 05a02d2..ad5bfde 100644 --- a/docs/specs/theme-polish/design.md +++ b/docs/specs/theme-polish/design.md @@ -17,10 +17,10 @@ See `requirements.md`; requirement ids (R1.1, …) are referenced below. ## Login theme (`apps/keycloak/themes/mancala/login`) -- `theme.properties`: `darkMode=false`, `scripts=js/theme.js`, styles `css/styles.css css/mancala.css`. +- `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/mancala.css`: the Bricolage Grotesque font; the tokens of `.t-ceviz`, `.t-gece`, `.t-yesim`; PatternFly's +- `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. -- 2.45.2 From 12538a0ef8e58f780f0f56802244d19b0eed6fe1 Mon Sep 17 00:00:00 2001 From: Halit Aksoy Date: Sat, 17 Oct 2026 03:28:04 +0300 Subject: [PATCH 8/8] docs(todo): list what documenting the deployment needs The Jenkins job's steps live only in Jenkins; note the image builds, the env file, the nginx block and the checks the Java backend needs. --- TODO.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/TODO.md b/TODO.md index 89cc2e9..edd8643 100644 --- a/TODO.md +++ b/TODO.md @@ -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 -- 2.45.2