Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
e3c1729
chore(SKY-47): add Keycloak restart policy and harden init-databases …
Krywion May 10, 2026
df19358
feat(SKY-47): mount custom Keycloak themes dir and disable theme cach…
Krywion May 10, 2026
58f043d
fix(SKY-47): scope Keycloak themes mount to subdirectory to preserve …
Krywion May 10, 2026
0c779e3
feat(SKY-47): activate skyroster Keycloak login theme inheriting keyc…
Krywion May 10, 2026
ad10931
feat(SKY-47): enable Polish i18n with Polish as default locale for lo…
Krywion May 10, 2026
1a7f563
docs(SKY-47): document IGNORE_EXISTING gotcha for realm re-import
Krywion May 10, 2026
624a225
feat(SKY-47): add Aura-derived design tokens and base body styling
Krywion May 10, 2026
ab705a8
fix(SKY-47): add -moz-osx-font-smoothing for consistent Firefox rende…
Krywion May 10, 2026
c6c58e6
feat(SKY-47): self-host Inter font (Regular + SemiBold) for login theme
Krywion May 10, 2026
318fea3
fix(SKY-47): replace InterDisplay variant with text-optimized Inter f…
Krywion May 10, 2026
aba90f3
feat(SKY-47): override template.ftl with centered card layout and Sky…
Krywion May 15, 2026
fe5857a
fix(SKY-47): remove duplicate main landmark and unused macro parameter
Krywion May 15, 2026
b3e1040
feat(SKY-47): override login.ftl with custom form (username, password…
Krywion May 15, 2026
64261bd
fix(SKY-47): a11y improvements and focus-ring tokens for login form
Krywion May 15, 2026
561af87
feat(SKY-47): add custom error.ftl with branded layout and back-to-lo…
Krywion May 15, 2026
dcc3666
fix(SKY-47): dynamic per-screen subtitle and proper error page back link
Krywion May 15, 2026
9076c27
fix(SKY-47): add vertical gap between form fields and submit button
Krywion May 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion infrastructure/local/docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,20 @@ services:
- "8180:8080"
volumes:
- ./keycloak/realm-export.json:/opt/keycloak/data/import/realm-export.json
command: start-dev --import-realm
- ./keycloak/themes/skyroster:/opt/keycloak/themes/skyroster
# NOTE: --import-realm uses IGNORE_EXISTING strategy. Changes to realm-export.json
# are NOT re-applied on existing realms. To pick up changes locally, run:
# docker compose down -v && docker compose --profile <profile> up -d
command: >
start-dev
--import-realm
--spi-theme-cache-themes=false
--spi-theme-cache-templates=false
--spi-theme-static-max-age=-1
depends_on:
postgres:
condition: service_healthy
restart: on-failure:10

frontend:
build:
Expand Down
4 changes: 4 additions & 0 deletions infrastructure/local/keycloak/realm-export.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
"realm": "skyroster",
"enabled": true,
"sslRequired": "none",
"loginTheme": "skyroster",
"internationalizationEnabled": true,
"supportedLocales": ["pl", "en"],
"defaultLocale": "pl",
"roles": {
"realm": [
{ "name": "operations_administrator", "composite": false },
Expand Down
15 changes: 15 additions & 0 deletions infrastructure/local/keycloak/themes/skyroster/login/error.ftl
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<#import "template.ftl" as layout>
<@layout.registrationLayout displayMessage=false; section>
<#if section = "header">
${msg("errorTitle")}
<#elseif section = "form">
<div class="sr-alert sr-alert--error" role="alert">
<span>${kcSanitize(message.summary)?no_esc}</span>
</div>
<#if client?? && client.baseUrl?has_content>
<a href="${client.baseUrl}" class="sr-button sr-button--secondary">${msg("backToLogin")}</a>
<#else>
<a href="${url.loginRestartFlowUrl!''}" class="sr-button sr-button--secondary">${msg("backToLogin")}</a>
</#if>
</#if>
</@layout.registrationLayout>
59 changes: 59 additions & 0 deletions infrastructure/local/keycloak/themes/skyroster/login/login.ftl
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<#import "template.ftl" as layout>
<@layout.registrationLayout displayMessage=!messagesPerField.existsError('username','password'); section>
<#if section = "header">
${msg("loginAccountTitle")}
<#elseif section = "form">
<form id="kc-form-login" action="${url.loginAction}" method="post" novalidate="novalidate">

<div class="sr-field">
<label for="username" class="sr-label">${msg("usernameOrEmail")}</label>
<input
id="username"
class="sr-input"
name="username"
value="${(login.username!'')}"
type="text"
autofocus
autocomplete="username"
aria-invalid="${(messagesPerField.existsError('username','password'))?c}"
/>
<span class="sr-field__error" aria-live="polite">
<#if messagesPerField.existsError('username','password')>
${kcSanitize(messagesPerField.getFirstError('username','password'))?no_esc}
</#if>
</span>
</div>

<div class="sr-field">
<label for="password" class="sr-label">${msg("password")}</label>
<input
id="password"
class="sr-input"
name="password"
type="password"
autocomplete="current-password"
aria-invalid="${(messagesPerField.existsError('username','password'))?c}"
/>
</div>

<#if realm.rememberMe && !usernameHidden??>
<div class="sr-checkbox">
<input id="rememberMe" name="rememberMe" type="checkbox"
<#if login.rememberMe??>checked</#if>
/>
<label for="rememberMe">${msg("rememberMe")}</label>
</div>
</#if>

<input type="hidden" id="id-hidden-input" name="credentialId" <#if auth.selectedCredential?has_content>value="${auth.selectedCredential}"</#if>/>

<button
class="sr-button sr-button--primary"
name="login"
id="kc-login"
type="submit"
>${msg("doLogIn")}</button>

</form>
</#if>
</@layout.registrationLayout>
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
loginAccountTitle=Sign in to SkyRoster
backToLogin=Back to login
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
loginAccountTitle=Zaloguj się do SkyRoster
usernameOrEmail=Nazwa użytkownika
password=Hasło
doLogIn=Zaloguj się
rememberMe=Zapamiętaj mnie
errorTitle=Wystąpił błąd
backToLogin=Wróć do logowania
invalidUserMessage=Nieprawidłowa nazwa użytkownika lub hasło
accountDisabledMessage=Konto jest wyłączone, skontaktuj się z administratorem
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url("../fonts/Inter-Regular.woff2") format("woff2");
}

@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 600;
font-display: swap;
src: url("../fonts/Inter-SemiBold.woff2") format("woff2");
}

:root {
--sr-primary-500: #10b981;
--sr-primary-600: #059669;
--sr-primary-50: #ecfdf5;

--sr-surface-0: #ffffff;
--sr-surface-50: #f8fafc;
--sr-surface-100: #f1f5f9;
--sr-surface-200: #e2e8f0;
--sr-surface-700: #334155;
--sr-surface-900: #0f172a;

--sr-text: var(--sr-surface-900);
--sr-text-muted: var(--sr-surface-700);
--sr-border: var(--sr-surface-200);
--sr-danger: #ef4444;

--sr-radius-input: 6px;
--sr-radius-card: 12px;
--sr-shadow-card: 0 10px 30px rgba(15, 23, 42, 0.08);
--sr-ring: rgba(16, 185, 129, 0.2);
--sr-ring-strong: rgba(16, 185, 129, 0.35);

--sr-font: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}

html, body {
margin: 0;
padding: 0;
font-family: var(--sr-font);
color: var(--sr-text);
background: linear-gradient(180deg, var(--sr-surface-50) 0%, var(--sr-surface-100) 100%);
min-height: 100vh;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

* { box-sizing: border-box; }

.sr-card-wrapper {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
}

.sr-card {
width: 100%;
max-width: 420px;
background: var(--sr-surface-0);
border-radius: var(--sr-radius-card);
box-shadow: var(--sr-shadow-card);
padding: 32px;
}

.sr-card__header {
margin-bottom: 24px;
}

.sr-brand {
margin: 0;
font-size: 24px;
font-weight: 600;
color: var(--sr-text);
letter-spacing: -0.01em;
}

.sr-card__subtitle {
margin: 4px 0 0;
color: var(--sr-text-muted);
font-size: 14px;
}

.sr-card__body {
display: flex;
flex-direction: column;
gap: 16px;
}

.sr-alert {
padding: 10px 12px;
border-radius: var(--sr-radius-input);
font-size: 14px;
margin-bottom: 16px;
}

.sr-alert--error {
background: #fef2f2;
color: var(--sr-danger);
border: 1px solid #fecaca;
}

.sr-alert--success {
background: var(--sr-primary-50);
color: var(--sr-primary-600);
border: 1px solid #a7f3d0;
}

.sr-alert--warning {
background: #fffbeb;
color: #b45309;
border: 1px solid #fde68a;
}

.sr-alert--info {
background: var(--sr-surface-50);
color: var(--sr-text-muted);
border: 1px solid var(--sr-border);
}

@media (max-width: 480px) {
.sr-card-wrapper { padding: 16px; }
.sr-card { padding: 24px; }
}

#kc-form-login {
display: flex;
flex-direction: column;
gap: 16px;
}

.sr-field {
display: flex;
flex-direction: column;
gap: 6px;
}

.sr-field__error:empty {
display: none;
}

.sr-label {
font-size: 13px;
font-weight: 600;
color: var(--sr-text);
}

.sr-input {
width: 100%;
padding: 10px 12px;
font-size: 14px;
font-family: var(--sr-font);
color: var(--sr-text);
background: var(--sr-surface-0);
border: 1px solid var(--sr-border);
border-radius: var(--sr-radius-input);
transition: border-color 120ms, box-shadow 120ms;
}

.sr-input:focus {
outline: none;
border-color: var(--sr-primary-500);
box-shadow: 0 0 0 3px var(--sr-ring);
}

.sr-input[aria-invalid="true"] {
border-color: var(--sr-danger);
}

.sr-field__error {
font-size: 12px;
color: var(--sr-danger);
}

.sr-checkbox {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: var(--sr-text-muted);
}

.sr-checkbox input[type="checkbox"] {
accent-color: var(--sr-primary-500);
width: 16px;
height: 16px;
}

.sr-button {
font-family: var(--sr-font);
font-size: 14px;
font-weight: 600;
border-radius: var(--sr-radius-input);
cursor: pointer;
transition: background-color 120ms, border-color 120ms, transform 60ms;
border: 1px solid transparent;
}

.sr-button:active { transform: translateY(1px); }

.sr-button--primary {
width: 100%;
height: 44px;
background: var(--sr-primary-500);
color: #ffffff;
}

.sr-button--primary:hover { background: var(--sr-primary-600); }

.sr-button--primary:focus-visible {
outline: none;
box-shadow: 0 0 0 3px var(--sr-ring-strong);
}

.sr-button--secondary {
display: inline-flex;
align-items: center;
justify-content: center;
width: 100%;
height: 44px;
background: var(--sr-surface-0);
color: var(--sr-primary-600);
border: 1px solid var(--sr-border);
text-decoration: none;
}

.sr-button--secondary:hover {
background: var(--sr-surface-50);
border-color: var(--sr-primary-500);
}

.sr-button--secondary:focus-visible {
outline: none;
box-shadow: 0 0 0 3px var(--sr-ring-strong);
}
Binary file not shown.
Binary file not shown.
Loading
Loading