feat(keycloak): add a login theme in the game's look
The mancala login theme extends keycloak.v2 with one stylesheet: the game's app bar and logo, the board's colors, pit-shaped inputs, pill buttons and a board with stones on top of the card. Dark colors follow the system color scheme. The realm turns the theme on and offers English and Turkish. The test container now loads the theme and checks that the login page uses it in the requested language.
This commit is contained in:
parent
bb32989279
commit
d52960fa0a
@ -14,7 +14,7 @@ Online Mancala (the Turkish *Mangala* rules) with a web app, an Android/iOS app
|
||||
| `apps/backend` | Express + `ws`: matchmaking, games, auth, saved games, HTTP API |
|
||||
| `apps/frontend` | React web app, built with Parcel |
|
||||
| `apps/mobile` | React Native app |
|
||||
| `apps/keycloak` | Keycloak image with the realm and a Java extension for guest login |
|
||||
| `apps/keycloak` | Keycloak image with the realm, the login theme and a Java extension for guest login |
|
||||
| `apps/postgres` | Init script creating the `keycloak` and `mancala` databases |
|
||||
| `apps/compose.yaml` | Postgres, Valkey, Keycloak, backend, frontend |
|
||||
| `config/nginx.conf` | The production server's nginx config (Certbot managed) |
|
||||
@ -96,6 +96,9 @@ There is no automated end-to-end suite. Changes to the game flow were checked by
|
||||
- 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.
|
||||
|
||||
**Mobile**
|
||||
- `apps/mobile/release_android.sh` contains signing passwords (see `TODO.md`); do not copy them anywhere.
|
||||
|
||||
2
TODO.md
2
TODO.md
@ -34,6 +34,8 @@ The web app has game history (see `docs/specs/game-history`); the mobile app nee
|
||||
- [ ] Let guests turn their guest user into an account without changing identity (Keycloak required action)
|
||||
- [ ] 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
|
||||
|
||||
@ -18,5 +18,6 @@ FROM quay.io/keycloak/keycloak:26.7.5
|
||||
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/
|
||||
ENTRYPOINT ["/opt/keycloak/bin/kc.sh"]
|
||||
CMD ["start", "--optimized", "--import-realm"]
|
||||
|
||||
@ -33,6 +33,24 @@ are not asked for an email or a name.
|
||||
Keycloak imports the file only if the realm does not exist yet. After changing it, apply the change in the admin
|
||||
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;
|
||||
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
|
||||
(it survives restarts) and restart:
|
||||
|
||||
```sh
|
||||
docker cp apps/keycloak/themes/mancala apps-keycloak-1:/opt/keycloak/themes/
|
||||
docker exec apps-keycloak-1 rm -rf /opt/keycloak/data/tmp/kc-gzip-cache
|
||||
docker restart apps-keycloak-1
|
||||
```
|
||||
|
||||
## Running
|
||||
|
||||
```sh
|
||||
@ -59,7 +77,8 @@ cd apps/keycloak/extension
|
||||
mvn test
|
||||
```
|
||||
|
||||
The tests start Keycloak with the extension and the real realm file in a container, so they need Docker.
|
||||
The tests start Keycloak with the extension, the real realm file and the login theme in a container, so they need
|
||||
Docker.
|
||||
|
||||
## Upgrading Keycloak
|
||||
|
||||
|
||||
@ -27,6 +27,7 @@ import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.keycloak.admin.client.resource.RealmResource;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.testcontainers.utility.MountableFile;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
@ -34,7 +35,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import dasniko.testcontainers.keycloak.KeycloakContainer;
|
||||
|
||||
/**
|
||||
* Runs the extension and the real realm export in a Keycloak container. Needs Docker.
|
||||
* Runs the extension, the real realm export and the login theme in a Keycloak container. Needs Docker.
|
||||
*/
|
||||
class GuestLoginTest {
|
||||
|
||||
@ -50,6 +51,7 @@ class GuestLoginTest {
|
||||
keycloak = new KeycloakContainer("quay.io/keycloak/keycloak:26.7.5")
|
||||
.withProviderClassesFrom("target/classes")
|
||||
.withRealmImportFile("/mancala-realm.json")
|
||||
.withCopyFileToContainer(MountableFile.forHostPath("../themes/mancala"), "/opt/keycloak/themes/mancala")
|
||||
.withEnv("KC_SPI_AUTHENTICATOR__MANCALA_GUEST_AUTHENTICATOR__CLEANUP_INTERVAL_SECONDS", "2")
|
||||
.withEnv("KC_SPI_AUTHENTICATOR__MANCALA_GUEST_AUTHENTICATOR__CLEANUP_MIN_AGE_SECONDS", "0");
|
||||
keycloak.start();
|
||||
@ -87,6 +89,17 @@ class GuestLoginTest {
|
||||
assertTrue(response.body().contains("id=\"kc-form-login\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
void loginFormUsesTheMancalaThemeInTheRequestedLanguage() throws Exception {
|
||||
HttpResponse<String> response = newHttpClient().send(
|
||||
HttpRequest.newBuilder(authorizationUri(Map.of("ui_locales", "tr"), randomUrlSafe(32))).GET().build(),
|
||||
HttpResponse.BodyHandlers.ofString());
|
||||
|
||||
assertEquals(200, response.statusCode());
|
||||
assertTrue(response.body().contains("/login/mancala/css/mancala.css"), "mancala stylesheet missing");
|
||||
assertTrue(response.body().contains("lang=\"tr\""), "page is not in Turkish");
|
||||
}
|
||||
|
||||
@Test
|
||||
void cleanupRemovesGuestsWithoutSessionsAndKeepsEveryoneElse() throws Exception {
|
||||
String activeGuest = decodeJwt(loginAsGuest().get("access_token").asText()).get("preferred_username").asText();
|
||||
|
||||
@ -11,6 +11,10 @@
|
||||
"rememberMe": true,
|
||||
"verifyEmail": false,
|
||||
"bruteForceProtected": true,
|
||||
"loginTheme": "mancala",
|
||||
"internationalizationEnabled": true,
|
||||
"supportedLocales": ["en", "tr"],
|
||||
"defaultLocale": "en",
|
||||
"offlineSessionIdleTimeout": 2592000,
|
||||
"browserFlow": "browser with guest",
|
||||
"requiredActions": [
|
||||
|
||||
294
apps/keycloak/themes/mancala/login/resources/css/mancala.css
Normal file
294
apps/keycloak/themes/mancala/login/resources/css/mancala.css
Normal file
@ -0,0 +1,294 @@
|
||||
/*
|
||||
* 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 <html> 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);
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 312 76">
|
||||
<!-- A board at the start of a game: two stores and 2 x 6 pits with four stones each. Colors from DarkTheme. -->
|
||||
<defs>
|
||||
<g id="pit">
|
||||
<circle r="16" fill="#606060"/>
|
||||
<circle cx="-7.5" r="2.2" fill="#252525"/>
|
||||
<circle cx="-2.5" r="2.2" fill="#252525"/>
|
||||
<circle cx="2.5" r="2.2" fill="#252525"/>
|
||||
<circle cx="7.5" r="2.2" fill="#252525"/>
|
||||
</g>
|
||||
</defs>
|
||||
<rect x="0" y="0" width="28" height="76" rx="14" fill="#606060"/>
|
||||
<rect x="284" y="0" width="28" height="76" rx="14" fill="#606060"/>
|
||||
<use xlink:href="#pit" x="56" y="18"/>
|
||||
<use xlink:href="#pit" x="96" y="18"/>
|
||||
<use xlink:href="#pit" x="136" y="18"/>
|
||||
<use xlink:href="#pit" x="176" y="18"/>
|
||||
<use xlink:href="#pit" x="216" y="18"/>
|
||||
<use xlink:href="#pit" x="256" y="18"/>
|
||||
<use xlink:href="#pit" x="56" y="58"/>
|
||||
<use xlink:href="#pit" x="96" y="58"/>
|
||||
<use xlink:href="#pit" x="136" y="58"/>
|
||||
<use xlink:href="#pit" x="176" y="58"/>
|
||||
<use xlink:href="#pit" x="216" y="58"/>
|
||||
<use xlink:href="#pit" x="256" y="58"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
26
apps/keycloak/themes/mancala/login/resources/img/board.svg
Normal file
26
apps/keycloak/themes/mancala/login/resources/img/board.svg
Normal file
@ -0,0 +1,26 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 312 76">
|
||||
<!-- A board at the start of a game: two stores and 2 x 6 pits with four stones each. Colors from LightTheme. -->
|
||||
<defs>
|
||||
<g id="pit">
|
||||
<circle r="16" fill="#B8B8B8"/>
|
||||
<circle cx="-7.5" r="2.2" fill="#5B5B5B"/>
|
||||
<circle cx="-2.5" r="2.2" fill="#5B5B5B"/>
|
||||
<circle cx="2.5" r="2.2" fill="#5B5B5B"/>
|
||||
<circle cx="7.5" r="2.2" fill="#5B5B5B"/>
|
||||
</g>
|
||||
</defs>
|
||||
<rect x="0" y="0" width="28" height="76" rx="14" fill="#B8B8B8"/>
|
||||
<rect x="284" y="0" width="28" height="76" rx="14" fill="#B8B8B8"/>
|
||||
<use xlink:href="#pit" x="56" y="18"/>
|
||||
<use xlink:href="#pit" x="96" y="18"/>
|
||||
<use xlink:href="#pit" x="136" y="18"/>
|
||||
<use xlink:href="#pit" x="176" y="18"/>
|
||||
<use xlink:href="#pit" x="216" y="18"/>
|
||||
<use xlink:href="#pit" x="256" y="18"/>
|
||||
<use xlink:href="#pit" x="56" y="58"/>
|
||||
<use xlink:href="#pit" x="96" y="58"/>
|
||||
<use xlink:href="#pit" x="136" y="58"/>
|
||||
<use xlink:href="#pit" x="176" y="58"/>
|
||||
<use xlink:href="#pit" x="216" y="58"/>
|
||||
<use xlink:href="#pit" x="256" y="58"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
BIN
apps/keycloak/themes/mancala/login/resources/img/favicon.ico
Normal file
BIN
apps/keycloak/themes/mancala/login/resources/img/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.9 KiB |
76
apps/keycloak/themes/mancala/login/resources/img/logo.svg
Normal file
76
apps/keycloak/themes/mancala/login/resources/img/logo.svg
Normal file
@ -0,0 +1,76 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="100.00002mm"
|
||||
height="100.00001mm"
|
||||
viewBox="0 0 100.00002 100.00001"
|
||||
version="1.1"
|
||||
id="svg316"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview318"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm"
|
||||
showgrid="false" />
|
||||
<defs
|
||||
id="defs313" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-230.50606,-73.754324)">
|
||||
<rect
|
||||
style="fill:#b3b3b3;stroke-width:1.03151"
|
||||
id="rect489-5-7-8"
|
||||
width="100"
|
||||
height="100"
|
||||
x="230.50607"
|
||||
y="73.754326"
|
||||
ry="27.328773"
|
||||
inkscape:export-filename="mancala.svg"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96" />
|
||||
<rect
|
||||
style="fill:#666666;fill-opacity:1;stroke-width:0.747434"
|
||||
id="rect642-6-2-0-7"
|
||||
width="35"
|
||||
height="35"
|
||||
x="281.50607"
|
||||
y="87.754311"
|
||||
ry="17.5" />
|
||||
<rect
|
||||
style="fill:#b3b3b3;stroke-width:0.747434"
|
||||
id="rect642-6-7-9-9-9"
|
||||
width="35"
|
||||
height="35"
|
||||
x="244.50607"
|
||||
y="87.754311"
|
||||
ry="17.5" />
|
||||
<rect
|
||||
style="fill:#666666;fill-opacity:1;stroke:none;stroke-width:0.747434;stroke-opacity:1"
|
||||
id="rect642-6-2-1-3-2"
|
||||
width="35"
|
||||
height="35"
|
||||
x="281.50607"
|
||||
y="124.75433"
|
||||
ry="17.5" />
|
||||
<rect
|
||||
style="fill:#666666;fill-opacity:1;stroke-width:1.05044"
|
||||
id="rect1550-0"
|
||||
width="35"
|
||||
height="70"
|
||||
x="244.50607"
|
||||
y="87.754311"
|
||||
ry="17.5" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
6
apps/keycloak/themes/mancala/login/theme.properties
Normal file
6
apps/keycloak/themes/mancala/login/theme.properties
Normal file
@ -0,0 +1,6 @@
|
||||
# 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.
|
||||
parent=keycloak.v2
|
||||
import=common/keycloak
|
||||
|
||||
styles=css/styles.css css/mancala.css
|
||||
Loading…
Reference in New Issue
Block a user