Port the ScadaLink CentralUI sidebar pattern into the OtOpcUa AdminUI: - Drop the top app-bar. Brand moves into the side rail's header — same visual rhythm as ScadaLink's NavMenu. - New NavSection.razor: collapsible eyebrow toggle (rail-eyebrow-toggle CSS) with a chevron + label. Mirrors ScadaLink/Components/Layout/NavSection. - New NavSidebar.razor: interactive island carrying the three section groups (Navigation / Scripting / Live) + session block. Marked @rendermode InteractiveServer; MainLayout itself stays static-rendered because layouts can't take a RenderFragment Body across an interactive boundary. - New wwwroot/js/nav-state.js: window.navState.get/.set persists the expanded-section list to the otopcua_nav cookie (one-year lifetime, SameSite=Lax). Same shape as ScadaLink's scadabridge_nav. - New LoginLayout.razor + @layout LoginLayout on Login.razor: the login page now renders without the side rail — clean centred card. - MainLayout.razor: slimmed down to the d-flex shell + hamburger toggle + <NavSidebar/> + @Body. - Login.razor: also drops the trailing "LDAP bind against the configured directory..." footer that the user asked to remove. - site.css: adds .side-rail .brand styles (mirrored from ScadaLink) and the .rail-eyebrow-toggle / .rail-eyebrow-chevron / .rail-section-body styles for the new collapsible UI. Auto-expand on page load: NavSidebar seeds the expanded set from the current URL's first path segment (in OnInitialized so it works even on the very first server render) and from the cookie (in OnAfterRenderAsync once JS interop is available). LocationChanged hooks keep the expanded state in sync as the user navigates between sections.
165 lines
4.5 KiB
CSS
165 lines
4.5 KiB
CSS
/* OtOpcUa Admin — view-specific layer over the technical-light theme (theme.css).
|
|
Tokens live in theme.css; this sheet only carries layout + the side rail. */
|
|
|
|
/* ── App shell: side rail + page ─────────────────────────────────────────── */
|
|
/* The outer flex direction is supplied by Bootstrap utilities on the wrapper
|
|
(`d-flex flex-column flex-lg-row`) so the mobile hamburger row stacks above
|
|
the rail on <lg viewports and the rail sits beside the page on lg+. */
|
|
.app-shell {
|
|
align-items: stretch;
|
|
min-height: calc(100vh - 3.3rem);
|
|
}
|
|
|
|
.app-shell .page {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
/* ── Side rail ───────────────────────────────────────────────────────────── */
|
|
.side-rail {
|
|
width: 220px;
|
|
flex: 0 0 220px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.15rem;
|
|
padding: 1rem 0.7rem;
|
|
background: var(--card);
|
|
border-right: 1px solid var(--rule-strong);
|
|
}
|
|
|
|
/* On lg+ keep the side rail pinned so it stays visible when content scrolls. */
|
|
@media (min-width: 992px) {
|
|
#sidebar-collapse {
|
|
position: sticky;
|
|
top: 0;
|
|
height: 100vh;
|
|
align-self: flex-start;
|
|
z-index: 1020;
|
|
}
|
|
}
|
|
|
|
/* When the side rail is collapsed under <lg viewports the Bootstrap collapse
|
|
container removes the fixed width; restore full width on mobile. */
|
|
@media (max-width: 991.98px) {
|
|
.side-rail {
|
|
width: 100%;
|
|
min-width: 100%;
|
|
max-width: 100%;
|
|
height: auto;
|
|
}
|
|
}
|
|
|
|
/* Brand block pinned at the top of the side rail. Mirrors ScadaLink's
|
|
.sidebar .brand styling — used now that the top app-bar was dropped. */
|
|
.side-rail .brand {
|
|
color: var(--ink);
|
|
font-size: 1.1rem;
|
|
font-weight: 600;
|
|
letter-spacing: 0.02em;
|
|
padding: 1rem;
|
|
border-bottom: 1px solid var(--rule);
|
|
margin-bottom: 0.4rem;
|
|
}
|
|
.side-rail .brand .mark { color: var(--accent); }
|
|
|
|
.rail-eyebrow {
|
|
font-size: 0.68rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.07em;
|
|
color: var(--ink-faint);
|
|
padding: 0.3rem 0.6rem;
|
|
}
|
|
|
|
/* Collapsible variant — rendered by NavSection.razor. Looks like .rail-eyebrow
|
|
plus a leading chevron; clicking flips chevron + expanded state. */
|
|
.rail-eyebrow-toggle {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.4rem;
|
|
width: 100%;
|
|
background: transparent;
|
|
border: 0;
|
|
text-align: left;
|
|
font-size: 0.68rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.07em;
|
|
color: var(--ink-faint);
|
|
padding: 0.45rem 0.6rem 0.3rem;
|
|
cursor: pointer;
|
|
}
|
|
.rail-eyebrow-toggle:hover { color: var(--ink); }
|
|
.rail-eyebrow-chevron {
|
|
display: inline-block;
|
|
width: 0.7rem;
|
|
font-size: 0.55rem;
|
|
color: var(--ink-faint);
|
|
}
|
|
.rail-section-body {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.rail-link {
|
|
display: block;
|
|
padding: 0.4rem 0.6rem;
|
|
border-radius: 4px;
|
|
border-left: 2px solid transparent;
|
|
font-size: 0.86rem;
|
|
color: var(--ink-soft);
|
|
}
|
|
.rail-link:hover {
|
|
background: #f3f6fd;
|
|
color: var(--ink);
|
|
text-decoration: none;
|
|
}
|
|
.rail-link.active {
|
|
background: #eef2fc;
|
|
border-left-color: var(--accent);
|
|
color: var(--accent-deep);
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* ── Session block, pinned to the rail foot ──────────────────────────────── */
|
|
.rail-foot {
|
|
margin-top: auto;
|
|
padding-top: 0.6rem;
|
|
border-top: 1px solid var(--rule);
|
|
}
|
|
.rail-user {
|
|
display: block;
|
|
padding: 0 0.6rem;
|
|
font-weight: 600;
|
|
font-size: 0.88rem;
|
|
}
|
|
.rail-roles {
|
|
padding: 0.1rem 0.6rem 0.5rem;
|
|
font-family: var(--mono);
|
|
font-size: 0.72rem;
|
|
color: var(--ink-faint);
|
|
}
|
|
.rail-btn {
|
|
display: inline-block;
|
|
margin: 0 0.6rem;
|
|
padding: 0.3rem 0.7rem;
|
|
font-size: 0.78rem;
|
|
font-weight: 600;
|
|
color: var(--ink-soft);
|
|
background: var(--card);
|
|
border: 1px solid var(--rule-strong);
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
}
|
|
.rail-btn:hover {
|
|
border-color: var(--accent);
|
|
color: var(--accent);
|
|
text-decoration: none;
|
|
}
|
|
|
|
/* ── Login card centring ─────────────────────────────────────────────────── */
|
|
.login-wrap {
|
|
max-width: 380px;
|
|
margin: 3.5rem auto 0;
|
|
}
|