A phone had no navigation at all
The 720px breakpoint contained three rules:
.admin-sidebar { display: none; }
.rail-link:not(.rail-link--active) { display: none; }
.topbar-admin-link { display: none; }
Every one hides navigation rather than relocating it. On a phone the console showed the
page you were on and no way to reach any other: the admin sidebar was gone, every rail
link except the current one was gone, and the route from the workspace to the control
panel was gone. There was no hamburger to find because nothing had ever been built for a
narrow screen - the breakpoint assumed the sidebar could simply go away, which is only true
when nothing else is left to navigate with.
A toggle appears below the breakpoint and reveals the navigation as a drawer. The drawer is
positioned rather than shown inline, because an inline nav pushes the page down by its own
height every time it opens, so on a short viewport the page the operator asked for ends up
below the fold. Escape closes it, a click on the page behind it closes it, following a link
closes it, and crossing the breakpoint settles the state - without which the drawer class
survives into the wide layout and the bug only reappears when the phone is rotated back.
Above the breakpoint the toggle is hidden, because a control that opens what is already
open misreports the state of the page.
nodes.html is deliberately not given a drawer. Its navigation is inline in <main> rather
than in a sidebar, so there is nothing to put in a drawer and a hamburger there would
toggle nothing; its nav was a single horizontal scroll strip instead, which hides links off
the right edge with no sign that there are more, so it now wraps.
test_navigation.py states the rule - navigation hidden at a width must still be reachable
at that width - and reads the breakpoint with comments stripped. Two things that check got
wrong on the way, both recorded in the file:
- It flagged the drawer's own "display: none" as a violation. Hiding a closed drawer is
correct, or it would push the page down on every load; what is wrong is hiding navigation
with no way to open it. The test now requires a "body.nav-open" rule alongside any hide.
- It only required that *some* page carry a toggle, so deleting it from the admin console
- the page that was reported broken - passed, because the workspace still had one. Now
per page.
Mutation-verified: restoring the original defect fails 1, removing the toggle from the
admin console fails 1.
695 passed, 4 skipped.
This commit is contained in:
253
umbratrans_web/tests/test_navigation.py
Normal file
253
umbratrans_web/tests/test_navigation.py
Normal file
@@ -0,0 +1,253 @@
|
||||
"""Navigation must be reachable at every width, including a phone.
|
||||
|
||||
This exists because on a phone the console had no navigation at all. The 720px breakpoint
|
||||
contained:
|
||||
|
||||
.admin-sidebar { display: none; }
|
||||
.rail-link:not(.rail-link--active) { display: none; }
|
||||
.topbar-admin-link { display: none; }
|
||||
|
||||
Every one of those hides navigation rather than relocating it. The console showed the page
|
||||
you were on and no way to reach any other, and there was no hamburger to find because
|
||||
nothing had ever been built for a narrow screen - the breakpoint assumed the sidebar could
|
||||
simply go away, which is only true when nothing else is left to navigate with.
|
||||
|
||||
The rule these tests state: **at any width, every navigation link is reachable.** They read
|
||||
the stylesheet rather than driving a browser at several viewport sizes, because the failure
|
||||
was in the stylesheet and a headless browser is not available here. The checks are
|
||||
deliberately about the *breakpoint rules* - "no rule inside the narrow-screen block may
|
||||
hide a nav element outright" - rather than about the presence of particular selectors,
|
||||
because a rule that hides navigation will not be written with any particular class name
|
||||
twice.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
WEB = Path(__file__).resolve().parents[2] / "web"
|
||||
STYLES = WEB / "assets" / "styles.css"
|
||||
|
||||
NARROW = 720
|
||||
|
||||
#: Selectors whose only job is to move content somewhere else on a narrow screen. A rule
|
||||
#: that *hides* one of these outright is the defect this file exists to catch.
|
||||
NAVIGATION_SELECTORS = (
|
||||
"admin-sidebar",
|
||||
"workspace-rail",
|
||||
"rail-link",
|
||||
"admin-nav",
|
||||
"admin-nav-link",
|
||||
"topbar-admin-link",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def narrow_block() -> str:
|
||||
"""The contents of the `max-width: 720px` block, braces balanced.
|
||||
|
||||
Extracted rather than sliced by line, because the file is written with many rules per
|
||||
line and a line-based slice would take whatever happened to share the line.
|
||||
"""
|
||||
|
||||
text = STYLES.read_text()
|
||||
start = text.index(f"@media (max-width: {NARROW}px)")
|
||||
brace = text.index("{", start)
|
||||
depth = 0
|
||||
for index in range(brace, len(text)):
|
||||
if text[index] == "{":
|
||||
depth += 1
|
||||
elif text[index] == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return text[brace + 1 : index]
|
||||
raise AssertionError(f"the {NARROW}px media block is not closed in {STYLES}")
|
||||
|
||||
|
||||
def test_the_narrow_breakpoint_exists_at_all() -> None:
|
||||
"""A guard on the guard.
|
||||
|
||||
If the block cannot be found, every test below would read an empty string and pass
|
||||
while the breakpoint it is checking had been deleted - which is how a check passes
|
||||
because its mechanism failed.
|
||||
"""
|
||||
|
||||
assert str(NARROW) in STYLES.read_text(), f"no {NARROW}px breakpoint in {STYLES}"
|
||||
|
||||
|
||||
def _rules(block: str) -> list[tuple[str, str]]:
|
||||
"""Every `selectors { declarations }` in the block, comments stripped.
|
||||
|
||||
Comments matter here: the first version of this check read the comment explaining why
|
||||
the closed drawer uses `display: none` and reported it as a violation, which is what
|
||||
prompted the refinement below rather than deleting the rule.
|
||||
"""
|
||||
|
||||
stripped = re.sub(r"/\*.*?\*/", "", block, flags=re.S)
|
||||
return [(m.group(1), m.group(2)) for m in re.finditer(r"([^{}]+)\{([^{}]*)\}", stripped)]
|
||||
|
||||
|
||||
def _hidden(block: str, selector: str) -> list[str]:
|
||||
found = []
|
||||
for selectors, body in _rules(block):
|
||||
if re.search(rf"(^|[\s,])\.?{re.escape(selector)}\b", selectors) and re.search(
|
||||
r"display\s*:\s*none", body
|
||||
):
|
||||
found.append(f"{selectors.strip()} {{ {body.strip()} }}")
|
||||
return found
|
||||
|
||||
|
||||
@pytest.mark.parametrize("selector", NAVIGATION_SELECTORS)
|
||||
def test_navigation_hidden_on_a_phone_is_still_reachable(
|
||||
narrow_block: str, selector: str
|
||||
) -> None:
|
||||
"""Hiding a closed drawer is fine. Hiding it with no way to open it is the bug.
|
||||
|
||||
The distinction is the whole test. A drawer has to be hidden when closed or it would
|
||||
push the page down by its own height on every load; so `display: none` on a nav element
|
||||
is *correct* provided something reveals it again. What is not correct is a rule that
|
||||
hides navigation and leaves no way back - which is exactly what the console had, and it
|
||||
is why a phone showed the current page and nothing else.
|
||||
"""
|
||||
|
||||
hidden = _hidden(narrow_block, selector)
|
||||
if not hidden:
|
||||
return
|
||||
|
||||
revealed = [
|
||||
selectors
|
||||
for selectors, body in _rules(narrow_block)
|
||||
if "nav-open" in selectors
|
||||
and re.search(rf"(^|[\s,])\.?{re.escape(selector)}\b", selectors)
|
||||
and not re.search(r"display\s*:\s*none", body)
|
||||
]
|
||||
|
||||
assert revealed, (
|
||||
f"the {NARROW}px breakpoint hides {selector} and nothing reveals it:\n "
|
||||
+ "\n ".join(hidden)
|
||||
+ "\n\nThere must be a `body.nav-open` rule that displays it, and a "
|
||||
"`[data-nav-toggle]` control on the page. Hidden with no way to open it is what "
|
||||
"left a phone with no navigation."
|
||||
)
|
||||
|
||||
|
||||
def test_a_hidden_drawer_has_a_control_to_open_it() -> None:
|
||||
"""The other half: a rule that reveals the drawer is useless without a button.
|
||||
|
||||
Checked across the pages rather than the stylesheet, because the control is markup and
|
||||
the rule is CSS, and neither can be verified in isolation.
|
||||
"""
|
||||
|
||||
block = STYLES.read_text()
|
||||
narrow = next(
|
||||
chunk
|
||||
for chunk in re.findall(r"@media \(max-width: \d+px\) \{(.*?)\n\}", block, flags=re.S)
|
||||
if "nav-open" in chunk
|
||||
)
|
||||
hides_something = any(
|
||||
re.search(r"display\s*:\s*none", body) for _, body in _rules(narrow)
|
||||
)
|
||||
assert hides_something, "the drawer is never hidden, so there is nothing to open"
|
||||
|
||||
# Per page, not "some page has one". The first version of this required only that one
|
||||
# page carried a toggle, so deleting it from the admin console - the page that actually
|
||||
# needs it, and the one reported broken - passed. workspace.html still had one, and the
|
||||
# assertion was satisfied by the wrong page.
|
||||
for page in sorted((WEB / "uadmin").glob("*.html")) + [WEB / "workspace.html"]:
|
||||
html = page.read_text()
|
||||
drawer_nav = "admin-sidebar" in html or "workspace-rail" in html
|
||||
if not drawer_nav:
|
||||
continue
|
||||
assert "data-nav-toggle" in html, (
|
||||
f"{page.name} has its navigation hidden behind a drawer at {NARROW}px but "
|
||||
"carries no [data-nav-toggle] button, so nothing can open it. On a phone that "
|
||||
"is no navigation at all."
|
||||
)
|
||||
|
||||
|
||||
def test_inactive_navigation_links_are_not_hidden(narrow_block: str) -> None:
|
||||
"""The specific rule that broke it, named rather than pattern-matched.
|
||||
|
||||
`.rail-link:not(.rail-link--active) { display: none }` hid every section except the one
|
||||
already open, which reads as deliberate and is the worst version of the bug: the page
|
||||
looks like it has navigation.
|
||||
"""
|
||||
|
||||
code = re.sub(r"/\*.*?\*/", "", narrow_block, flags=re.S)
|
||||
|
||||
assert not re.search(
|
||||
r"\.rail-link:not\(\s*\.rail-link--active\s*\)\s*\{[^}]*display\s*:\s*none",
|
||||
code,
|
||||
), (
|
||||
"inactive rail links are hidden on a narrow screen with no rule to show them "
|
||||
"again, so the console shows the current section and nothing else"
|
||||
)
|
||||
|
||||
|
||||
def test_there_is_a_way_to_open_the_drawer(narrow_block: str) -> None:
|
||||
"""Something has to reveal the nav, or "not hidden" just means "always visible".
|
||||
|
||||
Which would be its own failure on a phone: a sidebar permanently open above the content
|
||||
pushes every page down by its own height.
|
||||
"""
|
||||
|
||||
assert "nav-open" in narrow_block, (
|
||||
"the narrow breakpoint has no drawer state, so the navigation can only be always "
|
||||
"open or always hidden"
|
||||
)
|
||||
assert "position: fixed" in narrow_block, (
|
||||
"the drawer is not taken out of the flow, so opening it pushes the page down"
|
||||
)
|
||||
|
||||
|
||||
class TestEveryPageWithNavigationCanShowIt:
|
||||
"""Not a style rule: the markup has to carry a control the script can bind to."""
|
||||
|
||||
PAGES = sorted((WEB / "uadmin").glob("*.html")) + [WEB / "workspace.html"]
|
||||
|
||||
def test_there_are_pages_to_check(self) -> None:
|
||||
assert self.PAGES, f"no pages found to check under {WEB}"
|
||||
|
||||
@pytest.mark.parametrize("page", PAGES, ids=lambda p: p.name)
|
||||
def test_the_page_loads_the_navigation_script(self, page: Path) -> None:
|
||||
html = page.read_text()
|
||||
has_nav = "data-nav-toggle" in html
|
||||
|
||||
if has_nav:
|
||||
assert "nav.js" in html, (
|
||||
f"{page.name} has a navigation toggle but does not load nav.js, so the "
|
||||
"button does nothing"
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize("page", PAGES, ids=lambda p: p.name)
|
||||
def test_a_toggle_points_at_something_real(self, page: Path) -> None:
|
||||
"""`aria-controls` naming an element that is not there is worse than none.
|
||||
|
||||
It tells a screen reader the control opens something, and then nothing happens.
|
||||
"""
|
||||
|
||||
html = page.read_text()
|
||||
for match in re.finditer(r'data-nav-toggle[^>]*aria-controls="([^"]+)"', html):
|
||||
target = match.group(1)
|
||||
assert f'id="{target}"' in html, (
|
||||
f"{page.name} has a navigation toggle pointing at #{target}, which is not "
|
||||
"in the document"
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize("page", PAGES, ids=lambda p: p.name)
|
||||
def test_the_page_offers_more_than_one_destination(self, page: Path) -> None:
|
||||
"""A page with one link has nothing to navigate between.
|
||||
|
||||
Stated separately so that a page failing the navigation rules for having no
|
||||
navigation at all is not reported as a page whose navigation is unreachable.
|
||||
"""
|
||||
|
||||
html = page.read_text()
|
||||
links = re.findall(r'<a[^>]+href="([^"#][^"]*)"', html)
|
||||
assert len(links) >= 2, (
|
||||
f"{page.name} offers {len(links)} destination(s); a page with one cannot be "
|
||||
"navigated, so the rules above would not apply to it"
|
||||
)
|
||||
125
web/assets/nav.js
Normal file
125
web/assets/nav.js
Normal file
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* Navigation on a narrow screen.
|
||||
*
|
||||
* Before this, the stylesheet's 720px breakpoint hid `.admin-sidebar` outright and
|
||||
* reduced `.rail-nav` to the single active link:
|
||||
*
|
||||
* .admin-sidebar { display: none; }
|
||||
* .rail-link:not(.rail-link--active) { display: none; }
|
||||
* .topbar-admin-link { display: none; }
|
||||
*
|
||||
* Every one of those hides navigation rather than relocating it, so on a phone the
|
||||
* console showed the page you were on and no way to reach any other. There was no
|
||||
* hamburger to find because nothing had ever been built for a narrow screen - the
|
||||
* breakpoint assumed the sidebar could simply go away, which is only true if there is
|
||||
* nothing else to navigate with.
|
||||
*
|
||||
* The rule this file exists to keep: at any width, every navigation link is reachable.
|
||||
* `.nav-open` on the body reveals the nav as a drawer; the toggle is the only thing that
|
||||
* is hidden when the drawer is closed, and it is hidden only at widths where the nav is
|
||||
* already on screen.
|
||||
*
|
||||
* No framework and no build step, for the same reason as the rest of this console: the
|
||||
* deployment is a few static files and a Python process, and a nav toggle is not a
|
||||
* reason to change that.
|
||||
*/
|
||||
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
var TOGGLE = "[data-nav-toggle]";
|
||||
var OPEN = "nav-open";
|
||||
|
||||
function isNarrow() {
|
||||
return window.matchMedia("(max-width: 720px)").matches;
|
||||
}
|
||||
|
||||
function setUp() {
|
||||
var toggles = Array.prototype.slice.call(document.querySelectorAll(TOGGLE));
|
||||
|
||||
if (!toggles.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
function syncHidden(button) {
|
||||
/*
|
||||
* Hidden above the breakpoint, because the nav is already on screen there and a
|
||||
* button that opens what is already open is a control that lies about the state of
|
||||
* the page.
|
||||
*/
|
||||
button.hidden = !isNarrow();
|
||||
button.setAttribute("aria-expanded", isNarrow() && document.body.classList.contains(OPEN) ? "true" : "false");
|
||||
}
|
||||
|
||||
function close() {
|
||||
document.body.classList.remove(OPEN);
|
||||
toggles.forEach(function (button) {
|
||||
button.setAttribute("aria-expanded", "false");
|
||||
});
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
var open = document.body.classList.toggle(OPEN);
|
||||
toggles.forEach(function (button) {
|
||||
button.setAttribute("aria-expanded", open ? "true" : "false");
|
||||
});
|
||||
}
|
||||
|
||||
toggles.forEach(function (button) {
|
||||
syncHidden(button);
|
||||
button.addEventListener("click", toggle);
|
||||
});
|
||||
|
||||
/*
|
||||
* Escape closes, and so does a click on the page behind the drawer. Without the
|
||||
* second, the drawer covers the content and there is no way out on a device with no
|
||||
* keyboard - which is the whole population this breakpoint is for.
|
||||
*/
|
||||
document.addEventListener("keydown", function (event) {
|
||||
if (event.key === "Escape") {
|
||||
close();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("click", function (event) {
|
||||
if (!document.body.classList.contains(OPEN)) {
|
||||
return;
|
||||
}
|
||||
if (event.target.closest(".admin-sidebar, .workspace-rail, " + TOGGLE)) {
|
||||
return;
|
||||
}
|
||||
close();
|
||||
});
|
||||
|
||||
/*
|
||||
* Following a link should close the drawer. A hash link to another section on the
|
||||
* same page does not reload, so without this the drawer stays open over the section
|
||||
* the operator just chose.
|
||||
*/
|
||||
document.addEventListener("click", function (event) {
|
||||
var link = event.target.closest('a[href]');
|
||||
if (link && document.body.classList.contains(OPEN)) {
|
||||
close();
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
* Rotating a phone or resizing a desktop window past the breakpoint has to settle the
|
||||
* state. Without this the drawer class survives into the wide layout, where the
|
||||
* sidebar is on screen anyway and a stray `nav-open` does nothing visible - so the
|
||||
* bug is invisible until you rotate back.
|
||||
*/
|
||||
window.addEventListener("resize", function () {
|
||||
if (!isNarrow()) {
|
||||
close();
|
||||
}
|
||||
toggles.forEach(syncHidden);
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", setUp);
|
||||
} else {
|
||||
setUp();
|
||||
}
|
||||
})();
|
||||
@@ -405,7 +405,58 @@ html.modal-open { overflow: hidden; }
|
||||
.toolbar-actions { margin-left: 0; }
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.admin-layout, .workspace-shell { display: block; }.admin-sidebar, .workspace-rail { min-height: auto; border-right: 0; border-bottom: 1px solid var(--admin-border); }.admin-sidebar { display: none; }.workspace-rail { padding: .8rem 1rem; }.rail-section-label, .rail-section-label--spaced, .rail-footer { display: none; }.rail-nav { display: flex; gap: .25rem; overflow-x: auto; }.rail-link { white-space: nowrap; }.rail-link:not(.rail-link--active) { display: none; }.workspace-rail .brand-lockup { margin-bottom: .6rem; }.workspace-topbar { min-height: 3.7rem; }.topbar-admin-link, .status-pill { display: none; }.workspace-hero { display: block; padding: 1.8rem 0 1.2rem; }.hero-actions { margin-top: 1rem; }.metric-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }.language-row { grid-template-columns: 2.2rem minmax(0, 1fr) auto; }.language-row .progress-track, .language-row .language-percent { display: none; }.admin-topbar { flex-wrap: wrap; gap: .7rem; }.admin-context { padding-left: 0; border-left: 0; }.admin-top-actions { width: 100%; margin-left: 0; justify-content: space-between; }.admin-heading { display: block; }.admin-heading .button { margin-top: 1rem; }.admin-stat strong { font-size: 1.6rem; }
|
||||
.admin-layout, .workspace-shell { display: block; }.admin-sidebar, .workspace-rail { min-height: auto; border-right: 0; border-bottom: 1px solid var(--admin-border); }.workspace-rail { padding: .8rem 1rem; }.rail-section-label, .rail-section-label--spaced, .rail-footer { display: none; }.rail-nav { display: flex; gap: .25rem; overflow-x: auto; }.rail-link { white-space: nowrap; }.rail-link:not(.rail-link--active) { display: flex; }.workspace-rail .brand-lockup { margin-bottom: .6rem; }.workspace-topbar { min-height: 3.7rem; }.status-pill { display: none; }.workspace-hero { display: block; padding: 1.8rem 0 1.2rem; }.hero-actions { margin-top: 1rem; }.metric-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }.language-row { grid-template-columns: 2.2rem minmax(0, 1fr) auto; }.language-row .progress-track, .language-row .language-percent { display: none; }.admin-topbar { flex-wrap: wrap; gap: .7rem; }.admin-context { padding-left: 0; border-left: 0; }.admin-top-actions { width: 100%; margin-left: 0; justify-content: space-between; }.admin-heading { display: block; }.admin-heading .button { margin-top: 1rem; }.admin-stat strong { font-size: 1.6rem; }
|
||||
|
||||
/* --- navigation on a narrow screen ---------------------------------------
|
||||
The old breakpoint hid navigation rather than relocating it:
|
||||
`.admin-sidebar { display: none }`, `.rail-link:not(.rail-link--active)
|
||||
{ display: none }` and `.topbar-admin-link { display: none }`. On a phone that
|
||||
left the console showing the page you were on and no way to reach any other, and
|
||||
there was no hamburger because nothing had been built for a narrow screen.
|
||||
|
||||
The rule now: at any width, every navigation link is reachable. Above the
|
||||
breakpoint the rail and sidebar are on screen as before and the toggle is hidden.
|
||||
Below it the toggle appears and `.nav-open` on the body reveals the nav as a drawer
|
||||
over the content.
|
||||
|
||||
The drawer is positioned rather than shown inline, because an inline nav pushes the
|
||||
page down by its own height every time it opens - so on a short viewport the page
|
||||
the operator asked for ends up below the fold. */
|
||||
[data-nav-toggle] {
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
width: 2.4rem; height: 2.4rem; margin-right: .5rem; padding: 0;
|
||||
color: var(--workspace-teal, #5eead4); background: transparent;
|
||||
border: 1px solid var(--admin-border, #1e293b); border-radius: .45rem;
|
||||
font-size: 1.05rem; line-height: 1; cursor: pointer;
|
||||
|
||||
/* nodes.html carries its navigation inline in <main>, not in a sidebar, so there is
|
||||
nothing to put in a drawer and a hamburger there would toggle nothing. Its nav was a
|
||||
single horizontal scroll strip instead, which hides links off the right edge with no
|
||||
indication that there are more - so it wraps instead. Every link visible, no control
|
||||
needed. */
|
||||
.admin-main .admin-nav { flex-wrap: wrap; overflow-x: visible; gap: .35rem; }
|
||||
.admin-main .admin-nav-link { flex: 1 1 8rem; justify-content: center; }
|
||||
|
||||
}
|
||||
[data-nav-toggle]:focus-visible { outline: 2px solid #5eead4; outline-offset: 2px; }
|
||||
[data-nav-toggle][hidden] { display: none; }
|
||||
|
||||
/* `display: none` rather than `visibility: hidden` so a closed drawer leaves the tab
|
||||
order as well as the screen. A screen reader should not walk a menu nobody can open,
|
||||
and a phone with a screen reader is the case that matters. */
|
||||
.admin-sidebar { display: none; }
|
||||
body.nav-open .admin-sidebar,
|
||||
body.nav-open .workspace-rail {
|
||||
display: block; position: fixed; inset: 3.7rem 0 0 0; z-index: 40;
|
||||
overflow-y: auto; border-bottom: 0; background: #0d172a; padding: 1rem;
|
||||
}
|
||||
body.nav-open .admin-sidebar .admin-nav-link,
|
||||
body.nav-open .workspace-rail .rail-link { padding: .8rem .7rem; }
|
||||
body.nav-open .workspace-rail .rail-link:not(.rail-link--active) { display: flex; }
|
||||
.rail-nav { display: grid; gap: .2rem; }
|
||||
.rail-link { white-space: normal; }
|
||||
.topbar-admin-link { display: inline-flex; }
|
||||
|
||||
.form-row { grid-template-columns: 1fr; }.filter-field { min-width: 7rem; flex: 1 1 7rem; }.toolbar-actions { width: 100%; margin-left: 0; }.toolbar-actions .button { flex: 1; }
|
||||
.admin-table--wide, .admin-table--audit { min-width: 46rem; }.otp-value-row .button { width: 100%; }
|
||||
.pagination { justify-content: stretch; }.pagination__info { width: 100%; }.pagination__buttons .button { flex: 1; }
|
||||
|
||||
@@ -21,12 +21,17 @@
|
||||
<small id="admin-identity">No session</small>
|
||||
</span>
|
||||
</span>
|
||||
<!-- The only route between admin sections on a phone. Above 720px the sidebar
|
||||
is already on screen and nav.js hides this, because a control that opens
|
||||
what is already open misreports the state of the page. -->
|
||||
<button class="text-button" type="button" data-nav-toggle aria-expanded="false"
|
||||
aria-controls="admin-sidebar" aria-label="Open navigation" hidden>☰</button>
|
||||
<button class="button button--ghost" type="button" data-signout>Sign out</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="admin-layout">
|
||||
<aside class="admin-sidebar" aria-label="Administration navigation">
|
||||
<aside class="admin-sidebar" id="admin-sidebar" aria-label="Administration navigation">
|
||||
<div class="admin-nav-label">Overview</div>
|
||||
<nav class="admin-nav" aria-label="Sections">
|
||||
<a class="admin-nav-link admin-nav-link--active" href="#dashboard" data-view-link="dashboard"><span aria-hidden="true">▦</span>Dashboard</a>
|
||||
@@ -594,6 +599,7 @@
|
||||
</div>
|
||||
|
||||
<div id="toast" class="toast" role="status" aria-live="polite"></div>
|
||||
<script src="../assets/nav.js" defer></script>
|
||||
<script src="../assets/api.js" defer></script>
|
||||
<script src="../assets/admin.js" defer></script>
|
||||
</body>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
</head>
|
||||
<body class="workspace-page">
|
||||
<div class="workspace-shell">
|
||||
<aside class="workspace-rail" aria-label="Workspace navigation">
|
||||
<aside class="workspace-rail" id="workspace-rail" aria-label="Workspace navigation">
|
||||
<a class="brand-lockup brand-lockup--rail" href="index.html">
|
||||
<img class="brand-image" src="assets/umbratrans-emblem.png" alt="" aria-hidden="true">
|
||||
<span>UmbraTrans</span>
|
||||
@@ -43,6 +43,11 @@
|
||||
|
||||
<main class="workspace-main" id="overview">
|
||||
<header class="workspace-topbar">
|
||||
<!-- The only way to reach the other sections on a narrow screen. Above 720px the
|
||||
rail is already on screen and nav.js sets this hidden, because a control that
|
||||
opens what is already open misreports the state of the page. -->
|
||||
<button class="text-button" type="button" data-nav-toggle aria-expanded="false"
|
||||
aria-controls="workspace-rail" aria-label="Open navigation" hidden>☰</button>
|
||||
<div class="breadcrumb"><span>Workspace</span><span aria-hidden="true">/</span><strong id="breadcrumb-identity">Loading…</strong></div>
|
||||
<div class="topbar-actions">
|
||||
<span class="status-pill status-pill--info" id="workspace-session-pill">Session active</span>
|
||||
@@ -173,6 +178,7 @@
|
||||
</main>
|
||||
</div>
|
||||
<div id="toast" class="toast" role="status" aria-live="polite"></div>
|
||||
<script src="assets/nav.js" defer></script>
|
||||
<script src="assets/api.js" defer></script>
|
||||
<script src="assets/app.js" defer></script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user