diff --git a/ChessCubing.App/Components/SiteMenu.razor b/ChessCubing.App/Components/SiteMenu.razor
new file mode 100644
index 0000000..90dc54b
--- /dev/null
+++ b/ChessCubing.App/Components/SiteMenu.razor
@@ -0,0 +1,112 @@
+@using System.Security.Claims
+@inject NavigationManager Navigation
+
+
+
+@code {
+ private static readonly string[] HomePaths = ["", "index.html"];
+ private static readonly string[] ApplicationPaths = ["application", "application.html"];
+ private static readonly string[] RulesPaths = ["reglement", "reglement.html"];
+
+ private string LoginHref => BuildAuthHref("login", EffectiveReturnUrl);
+ private string RegisterHref => BuildAuthHref("register", EffectiveReturnUrl);
+ private string LogoutHref => BuildAuthHref("logout", "/");
+
+ private string CurrentPath
+ {
+ get
+ {
+ var absolutePath = new Uri(Navigation.Uri).AbsolutePath;
+ return absolutePath.Trim('/');
+ }
+ }
+
+ private string EffectiveReturnUrl
+ {
+ get
+ {
+ var absolutePath = new Uri(Navigation.Uri).AbsolutePath;
+ if (absolutePath.StartsWith("/authentication/", StringComparison.OrdinalIgnoreCase))
+ {
+ return "/";
+ }
+
+ return string.IsNullOrWhiteSpace(absolutePath)
+ ? "/"
+ : absolutePath;
+ }
+ }
+
+ private string BuildNavLinkClass(string[] paths)
+ => IsCurrentPage(paths) ? "site-menu-link is-active" : "site-menu-link";
+
+ private string? BuildAriaCurrent(string[] paths)
+ => IsCurrentPage(paths) ? "page" : null;
+
+ private bool IsCurrentPage(string[] paths)
+ => paths.Any(path => string.Equals(CurrentPath, path, StringComparison.OrdinalIgnoreCase));
+
+ private static string BuildAuthHref(string action, string returnUrl)
+ => $"authentication/{action}?returnUrl={Uri.EscapeDataString(returnUrl)}";
+
+ private static string BuildDisplayName(ClaimsPrincipal user)
+ => user.Identity?.Name
+ ?? user.FindFirst("name")?.Value
+ ?? user.FindFirst("preferred_username")?.Value
+ ?? "Utilisateur connecte";
+
+ private static string BuildMeta(ClaimsPrincipal user)
+ => user.FindFirst("email")?.Value
+ ?? "Session active";
+}
diff --git a/ChessCubing.App/Components/UserAccessBar.razor b/ChessCubing.App/Components/UserAccessBar.razor
deleted file mode 100644
index 2620f19..0000000
--- a/ChessCubing.App/Components/UserAccessBar.razor
+++ /dev/null
@@ -1,108 +0,0 @@
-@using System.Security.Claims
-@inject NavigationManager Navigation
-
-
-
-
-
- Compte Keycloak
- @BuildDisplayName(authState.User)
- @BuildMeta(authState.User)
-
-
-
-
-
-
-
- Compte Keycloak
- Connexion optionnelle
- Le site reste accessible sans connexion. Vous pouvez vous connecter ou creer un compte si besoin.
-
-
-
-
-
-
-
- Compte Keycloak
- Connexion optionnelle
- Le site reste accessible sans connexion. Chaque compte conserve son propre etat de match dans ce navigateur.
-
-
-
-
-
-
-@code {
- [Parameter]
- public string? ReturnUrl { get; set; }
-
- private string LoginHref => BuildAuthHref("login", EffectiveReturnUrl);
- private string RegisterHref => BuildAuthHref("register", EffectiveReturnUrl);
- private string LogoutHref => BuildAuthHref("logout", "/");
-
- private string EffectiveReturnUrl
- {
- get
- {
- if (!string.IsNullOrWhiteSpace(ReturnUrl))
- {
- return ReturnUrl!;
- }
-
- var relativePath = Navigation.ToBaseRelativePath(Navigation.Uri);
- if (string.IsNullOrWhiteSpace(relativePath))
- {
- return "/";
- }
-
- return relativePath.StartsWith("/", StringComparison.Ordinal)
- ? relativePath
- : $"/{relativePath}";
- }
- }
-
- private static string BuildAuthHref(string action, string returnUrl)
- => $"authentication/{action}?returnUrl={Uri.EscapeDataString(returnUrl)}";
-
- private static string BuildDisplayName(ClaimsPrincipal user)
- => user.Identity?.Name
- ?? user.FindFirst("name")?.Value
- ?? user.FindFirst("preferred_username")?.Value
- ?? "Utilisateur connecte";
-
- private static string BuildMeta(ClaimsPrincipal user)
- {
- var details = new List();
- var email = user.FindFirst("email")?.Value;
- if (!string.IsNullOrWhiteSpace(email))
- {
- details.Add(email);
- }
-
- var roles = user.FindAll("role")
- .Select(claim => claim.Value)
- .Where(value => !string.IsNullOrWhiteSpace(value))
- .Distinct(StringComparer.OrdinalIgnoreCase)
- .OrderBy(value => value, StringComparer.OrdinalIgnoreCase)
- .ToArray();
-
- if (roles.Length > 0)
- {
- details.Add($"Roles : {string.Join(", ", roles)}");
- }
-
- return details.Count > 0
- ? string.Join(" | ", details)
- : "Session authentifiee via Keycloak.";
- }
-}
diff --git a/ChessCubing.App/Layout/MainLayout.razor b/ChessCubing.App/Layout/MainLayout.razor
index 4f3f76d..200d7c3 100644
--- a/ChessCubing.App/Layout/MainLayout.razor
+++ b/ChessCubing.App/Layout/MainLayout.razor
@@ -1,3 +1,24 @@
@inherits LayoutComponentBase
+@inject NavigationManager Navigation
+
+@if (!HideGlobalMenu)
+{
+
+}
@Body
+
+@code {
+ private bool HideGlobalMenu
+ {
+ get
+ {
+ var currentPath = new Uri(Navigation.Uri).AbsolutePath.Trim('/');
+
+ return string.Equals(currentPath, "chrono", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(currentPath, "chrono.html", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(currentPath, "cube", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(currentPath, "cube.html", StringComparison.OrdinalIgnoreCase);
+ }
+ }
+}
diff --git a/ChessCubing.App/Pages/ApplicationPage.razor b/ChessCubing.App/Pages/ApplicationPage.razor
index da8e335..b8e469f 100644
--- a/ChessCubing.App/Pages/ApplicationPage.razor
+++ b/ChessCubing.App/Pages/ApplicationPage.razor
@@ -23,7 +23,6 @@
Accueil du site
Consulter le reglement
-
diff --git a/ChessCubing.App/Pages/Home.razor b/ChessCubing.App/Pages/Home.razor
index b1dba83..7e8f7ee 100644
--- a/ChessCubing.App/Pages/Home.razor
+++ b/ChessCubing.App/Pages/Home.razor
@@ -28,13 +28,6 @@
Ouvrir l'appli d'Ethan
Ouvrir l'appli de Brice
-
diff --git a/ChessCubing.App/Pages/RulesPage.razor b/ChessCubing.App/Pages/RulesPage.razor
index 0e6cb32..1c4fafc 100644
--- a/ChessCubing.App/Pages/RulesPage.razor
+++ b/ChessCubing.App/Pages/RulesPage.razor
@@ -32,7 +32,6 @@
Ouvrir l'application
Retour a l'accueil
-
diff --git a/README.md b/README.md
index 05090fc..9ef7c99 100644
--- a/README.md
+++ b/README.md
@@ -34,8 +34,7 @@ Le projet continue a exposer les routes historiques `index.html`, `application.h
L'application embarque maintenant une authentification OpenID Connect basee sur Keycloak.
- toutes les pages du site restent accessibles sans connexion
-- la page d'accueil et la page reglement affichent l'etat de session courant
-- la page d'accueil propose directement des actions `Se connecter` et `Creer un compte`
+- un menu general en haut des pages site et application regroupe la navigation et les actions `Se connecter` / `Creer un compte`
- l'action `Creer un compte` ouvre le parcours d'inscription natif de Keycloak
- les roles Keycloak du realm sont exposes dans l'application
- l'etat du match est isole par utilisateur dans le navigateur grace a une cle de stockage derivee du compte connecte
diff --git a/styles.css b/styles.css
index c79f969..5ac3534 100644
--- a/styles.css
+++ b/styles.css
@@ -1023,6 +1023,135 @@ body[data-page="cube"] .zone-button.cube-hold-ready::after {
padding: 1.2rem 0 2rem;
}
+.site-menu-shell {
+ position: sticky;
+ top: calc(var(--safe-top) + 0.75rem);
+ z-index: 40;
+ width: min(1220px, calc(100% - 2rem));
+ margin: 0 auto;
+ padding: 1rem 0 0;
+}
+
+.site-menu-bar {
+ border: 1px solid var(--panel-border);
+ border-radius: 26px;
+ background: rgba(14, 16, 21, 0.9);
+ box-shadow: var(--shadow);
+ backdrop-filter: blur(18px);
+}
+
+.site-menu-main {
+ display: grid;
+ grid-template-columns: auto minmax(0, 1fr) auto;
+ align-items: center;
+ gap: 1rem;
+ padding: 1rem 1.2rem;
+}
+
+.site-menu-brand {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.85rem;
+ color: var(--text);
+}
+
+.site-menu-brand:hover,
+.site-menu-link:hover {
+ text-decoration: none;
+}
+
+.site-menu-brand-icon {
+ width: 44px;
+ height: 44px;
+ object-fit: contain;
+ flex-shrink: 0;
+}
+
+.site-menu-brand-copy {
+ display: grid;
+ gap: 0.2rem;
+}
+
+.site-menu-brand-copy .micro-label {
+ margin-bottom: 0;
+}
+
+.site-menu-brand-copy strong {
+ font-size: 1rem;
+ line-height: 1;
+}
+
+.site-menu-links {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.55rem;
+}
+
+.site-menu-link {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0.7rem 0.95rem;
+ border: 1px solid transparent;
+ border-radius: 16px;
+ color: var(--muted);
+ background: rgba(255, 255, 255, 0.02);
+ font-weight: 800;
+ letter-spacing: 0.04em;
+ transition:
+ color 160ms ease,
+ background 160ms ease,
+ border-color 160ms ease,
+ transform 160ms ease;
+}
+
+.site-menu-link:hover {
+ color: var(--text);
+ border-color: var(--panel-border);
+ background: rgba(255, 255, 255, 0.06);
+ transform: translateY(-1px);
+}
+
+.site-menu-link.is-active {
+ color: var(--text);
+ border-color: rgba(52, 141, 255, 0.32);
+ background: rgba(17, 103, 255, 0.18);
+}
+
+.site-menu-account {
+ display: grid;
+ gap: 0.45rem;
+ justify-items: end;
+}
+
+.site-menu-account .micro-label {
+ margin-bottom: 0;
+}
+
+.site-menu-account-actions,
+.site-menu-account-panel {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: flex-end;
+ gap: 0.75rem;
+}
+
+.site-menu-user {
+ display: grid;
+ gap: 0.15rem;
+ text-align: right;
+}
+
+.site-menu-user strong {
+ font-size: 0.98rem;
+}
+
+.site-menu-user span {
+ color: var(--muted);
+ font-size: 0.9rem;
+}
+
.hero-rules h1 {
margin: 0;
font-size: clamp(2.25rem, 4.8vw, 3.8rem);
@@ -1047,48 +1176,6 @@ body[data-page="cube"] .zone-button.cube-hold-ready::after {
margin-top: 1.2rem;
}
-.hero-account-actions {
- display: grid;
- gap: 0.65rem;
- margin-top: 1rem;
-}
-
-.hero-account-buttons {
- display: flex;
- flex-wrap: wrap;
- gap: 0.75rem;
-}
-
-.user-access-bar {
- display: grid;
- gap: 0.9rem;
- margin-top: 1rem;
- padding: 1rem;
- border-radius: 24px;
- border: 1px solid var(--panel-border);
- background: rgba(12, 14, 20, 0.76);
-}
-
-.user-access-copy {
- display: grid;
- gap: 0.35rem;
-}
-
-.user-access-copy strong {
- font-size: 1.02rem;
-}
-
-.user-access-meta {
- color: var(--muted);
- line-height: 1.45;
-}
-
-.user-access-actions {
- display: flex;
- flex-wrap: wrap;
- gap: 0.75rem;
-}
-
.rule-metrics {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -1352,23 +1439,12 @@ body[data-page="cube"] .zone-button.cube-hold-ready::after {
text-align: center;
}
- .user-access-bar {
- grid-template-columns: 1fr;
- }
-
.brand-link,
.utility-button {
justify-self: center;
}
}
-@media (min-width: 720px) {
- .user-access-bar {
- grid-template-columns: minmax(0, 1fr) auto;
- align-items: center;
- }
-}
-
@media (max-width: 900px) {
.phase-body {
overflow-y: auto;
@@ -1379,6 +1455,7 @@ body[data-page="cube"] .zone-button.cube-hold-ready::after {
overscroll-behavior: none;
}
+ .site-menu-shell,
.setup-shell,
.phase-shell,
.rules-shell {
@@ -1390,6 +1467,32 @@ body[data-page="cube"] .zone-button.cube-hold-ready::after {
padding: 1rem;
}
+ .site-menu-main {
+ grid-template-columns: 1fr;
+ justify-items: stretch;
+ }
+
+ .site-menu-brand {
+ justify-content: center;
+ }
+
+ .site-menu-links {
+ justify-content: center;
+ }
+
+ .site-menu-account {
+ justify-items: stretch;
+ }
+
+ .site-menu-account-actions,
+ .site-menu-account-panel {
+ justify-content: flex-start;
+ }
+
+ .site-menu-user {
+ text-align: left;
+ }
+
.setup-form {
grid-template-columns: 1fr;
}