Release: Quarkus backend, Keycloak login, game history, clock, bot, tutorial and rematch #3
@ -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