mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-11-07 10:40:47 +00:00
Merge pull request #213 from okieselbach/fix/view-as-bar-layout
View as: keep the navbar and sidebar clear of the view-as bar
This commit is contained in:
@@ -22,6 +22,22 @@ html {
|
||||
drawer + overlay) keep sizing against the viewport. */
|
||||
overflow-x: clip;
|
||||
scrollbar-gutter: stable;
|
||||
/* Anchor jumps and scrollIntoView stop below the view-as bar as well (see the next block). */
|
||||
scroll-padding-top: var(--view-as-offset);
|
||||
}
|
||||
|
||||
/* ===== View-as bar (components/ViewAsBar.tsx) =====
|
||||
While a tab views a tenant as its member, or says once why that was refused, the bar sticks above the
|
||||
navbar at a fixed height. Everything positioned against the top of the viewport adds --view-as-offset —
|
||||
the navbar's sticky top, the desktop sidebar, sticky content, anchor scrolling — so nothing slides under
|
||||
the bar: the offset is the bar's height while it is shown and 0 otherwise. Offsets computed in JS read
|
||||
the bar's height through viewAsBarHeight() (lib/viewAs.ts). */
|
||||
:root {
|
||||
--view-as-bar-height: 2.25rem;
|
||||
--view-as-offset: 0px;
|
||||
}
|
||||
:root:has([data-view-as-bar]) {
|
||||
--view-as-offset: var(--view-as-bar-height);
|
||||
}
|
||||
|
||||
:root {
|
||||
|
||||
@@ -12,7 +12,7 @@ const ITEMS = [
|
||||
export function PushNav() {
|
||||
const pathname = (usePathname() ?? "/push").replace(/\/$/, "") || "/push";
|
||||
return (
|
||||
<header className="bg-white border-b border-gray-200 sticky top-0 z-10">
|
||||
<header className="bg-white border-b border-gray-200 sticky top-[var(--view-as-offset)] z-10">
|
||||
<div className="max-w-md mx-auto px-4 h-12 flex items-center justify-between">
|
||||
<Link href="/push" className="flex items-center gap-2 min-w-0">
|
||||
<BrandMark className="w-5 h-5 shrink-0" />
|
||||
|
||||
@@ -13,6 +13,7 @@ import { useAdminMode } from "../hooks/useAdminMode";
|
||||
import { useMediaQuery } from "../hooks/useMediaQuery";
|
||||
import { DOCS_URL } from "@/lib/config";
|
||||
import { isPushAppPath } from "@/lib/push/pushAppPath";
|
||||
import { viewAsBarHeight } from "@/lib/viewAs";
|
||||
|
||||
// Sidebar pixel widths
|
||||
export const SIDEBAR_PX: Record<CollapseState, number> = {
|
||||
@@ -75,7 +76,8 @@ export function GlobalSidebar({ children }: { children: ReactNode }) {
|
||||
}
|
||||
}
|
||||
},
|
||||
{ rootMargin: "-80px 0px -60% 0px", threshold: 0 },
|
||||
// The top 80 px are the navbar plus a margin; a view-as bar above the navbar adds its own height.
|
||||
{ rootMargin: `-${80 + viewAsBarHeight()}px 0px -60% 0px`, threshold: 0 },
|
||||
);
|
||||
|
||||
observerRef.current = observer;
|
||||
@@ -104,7 +106,8 @@ export function GlobalSidebar({ children }: { children: ReactNode }) {
|
||||
const scrollTo = useCallback((id: string) => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) {
|
||||
const y = el.getBoundingClientRect().top + window.scrollY - 90;
|
||||
// 90 px keep the section clear of the navbar; a view-as bar above the navbar adds its own height.
|
||||
const y = el.getBoundingClientRect().top + window.scrollY - 90 - viewAsBarHeight();
|
||||
window.scrollTo({ top: y, behavior: "smooth" });
|
||||
}
|
||||
setActiveSectionId(id);
|
||||
@@ -726,7 +729,7 @@ export function GlobalSidebar({ children }: { children: ReactNode }) {
|
||||
|
||||
{/* ===== Desktop: fixed sidebar ===== */}
|
||||
<aside
|
||||
className={`hidden md:flex fixed left-0 ${hasNavbar ? "top-14" : "top-0"} bottom-0 z-20 flex-col transition-all duration-200 ease-in-out overflow-hidden ${sidebarWidthClass[collapseState]}`}
|
||||
className={`hidden md:flex fixed left-0 ${hasNavbar ? "top-[calc(3.5rem+var(--view-as-offset))]" : "top-[var(--view-as-offset)]"} bottom-0 z-20 flex-col transition-all duration-200 ease-in-out overflow-hidden ${sidebarWidthClass[collapseState]}`}
|
||||
>
|
||||
{collapseState !== "hidden" && (
|
||||
<nav className="flex flex-col h-full bg-white border-r border-gray-200 dark:bg-gray-800 dark:border-gray-700">
|
||||
|
||||
@@ -144,7 +144,7 @@ export default function Navbar() {
|
||||
// scope → gets the full navbar.
|
||||
if (!isAdminOrOperator && !isViewer && !hasGlobalScope) {
|
||||
return (
|
||||
<nav className="bg-white border-b border-gray-200 shadow-sm sticky top-0 z-30">
|
||||
<nav className="bg-white border-b border-gray-200 shadow-sm sticky top-[var(--view-as-offset)] z-30">
|
||||
<div className="px-3">
|
||||
<div className="flex justify-between h-14">
|
||||
<div className="flex items-center">
|
||||
@@ -193,7 +193,7 @@ export default function Navbar() {
|
||||
}
|
||||
|
||||
return (
|
||||
<nav className="bg-white border-b border-gray-200 shadow-sm sticky top-0 z-30">
|
||||
<nav className="bg-white border-b border-gray-200 shadow-sm sticky top-[var(--view-as-offset)] z-30">
|
||||
<div className="px-3">
|
||||
<div className="flex justify-between h-14">
|
||||
{/* Logo and Title */}
|
||||
@@ -272,7 +272,7 @@ export default function Navbar() {
|
||||
(!!user?.isGlobalAdmin && visibleGlobal.length > 0);
|
||||
|
||||
return (
|
||||
<div className="fixed sm:absolute top-16 sm:top-auto left-2 right-2 sm:left-auto sm:right-0 mt-0 sm:mt-2 sm:w-96 bg-white rounded-lg shadow-lg border border-gray-200 z-50 max-h-[calc(100vh-5rem)] sm:max-h-96 overflow-hidden flex flex-col">
|
||||
<div className="fixed sm:absolute top-[calc(4rem+var(--view-as-offset))] sm:top-auto left-2 right-2 sm:left-auto sm:right-0 mt-0 sm:mt-2 sm:w-96 bg-white rounded-lg shadow-lg border border-gray-200 z-50 max-h-[calc(100vh-5rem-var(--view-as-offset))] sm:max-h-96 overflow-hidden flex flex-col">
|
||||
<div className="px-4 py-3 border-b border-gray-200 flex justify-between items-center">
|
||||
<h3 className="text-sm font-semibold text-gray-900">Notifications</h3>
|
||||
{(unreadCount > 0 || hasClearable) && (
|
||||
|
||||
@@ -10,7 +10,11 @@ const refusedNotice = typeof window === "undefined" ? null : takeViewAsNotice();
|
||||
const getRefusedNotice = () => refusedNotice;
|
||||
const noValue = () => null;
|
||||
|
||||
const BAR_CLASS = "bg-purple-700 text-white text-sm px-4 py-2 flex flex-wrap items-center justify-center gap-x-2 gap-y-1";
|
||||
// One line at a fixed height, sticky above the navbar. While an element carrying data-view-as-bar is shown,
|
||||
// globals.css moves everything positioned against the top of the viewport down by exactly that height, so the
|
||||
// bar never wraps: a long text truncates instead.
|
||||
const BAR_CLASS =
|
||||
"sticky top-0 z-30 h-[var(--view-as-bar-height)] overflow-hidden whitespace-nowrap bg-purple-700 text-white text-sm px-4 flex items-center justify-center gap-x-2";
|
||||
|
||||
/**
|
||||
* The one deliberate difference of a view-as tab from the customer's own portal: a slim bar that names the
|
||||
@@ -28,17 +32,17 @@ export function ViewAsBar() {
|
||||
// The backend's name for the tenant once auth/me answered; the id until then (or when it refused).
|
||||
const tenantName = user?.viewAs?.tenantName ?? selection.tenantId;
|
||||
return (
|
||||
<div className={BAR_CLASS} role="status">
|
||||
<div data-view-as-bar className={BAR_CLASS} role="status">
|
||||
<svg className="w-4 h-4 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||
</svg>
|
||||
<span className="font-medium">Viewing as {tenantName}</span>
|
||||
<span className="text-purple-300">— {selection.role}, read-only</span>
|
||||
<span className="min-w-0 truncate font-medium" title={tenantName}>Viewing as {tenantName}</span>
|
||||
<span className="flex-shrink-0 text-purple-300">— {selection.role}, read-only</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={exitViewAs}
|
||||
className="ml-2 rounded border border-purple-300 px-2 py-0.5 text-xs font-medium hover:bg-purple-600 focus:outline-none focus-visible:ring-2 focus-visible:ring-white"
|
||||
className="ml-2 flex-shrink-0 rounded border border-purple-300 px-2 py-0.5 text-xs font-medium hover:bg-purple-600 focus:outline-none focus-visible:ring-2 focus-visible:ring-white"
|
||||
>
|
||||
Exit
|
||||
</button>
|
||||
@@ -48,14 +52,14 @@ export function ViewAsBar() {
|
||||
|
||||
if (notice && !noticeDismissed) {
|
||||
return (
|
||||
<div className={BAR_CLASS} role="alert">
|
||||
<span className="font-medium">View as ended</span>
|
||||
<span className="text-purple-300">— {notice}</span>
|
||||
<div data-view-as-bar className={BAR_CLASS} role="alert">
|
||||
<span className="flex-shrink-0 font-medium">View as ended</span>
|
||||
<span className="min-w-0 truncate text-purple-300" title={notice}>— {notice}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setNoticeDismissed(true)}
|
||||
aria-label="Dismiss"
|
||||
className="ml-2 rounded px-1.5 text-purple-200 hover:text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-white"
|
||||
className="ml-2 flex-shrink-0 rounded px-1.5 text-purple-200 hover:text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-white"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
|
||||
@@ -41,7 +41,7 @@ export default function SeverityPriorityCard({
|
||||
return (
|
||||
<section
|
||||
aria-label="Severity and remediation priority"
|
||||
className="bg-white rounded-lg shadow lg:col-span-2 lg:sticky lg:top-20"
|
||||
className="bg-white rounded-lg shadow lg:col-span-2 lg:sticky lg:top-[calc(5rem+var(--view-as-offset))]"
|
||||
>
|
||||
<div className="p-5">
|
||||
<h3 className="text-base font-semibold text-gray-900">Exposure by severity</h3>
|
||||
|
||||
@@ -88,6 +88,14 @@ export function viewAsHeaders(): Record<string, string> {
|
||||
return selection ? { "X-View-As-Tenant": selection.tenantId, "X-View-As-Role": selection.role } : {};
|
||||
}
|
||||
|
||||
/**
|
||||
* How far the view-as bar (components/ViewAsBar.tsx) pushes the navbar down right now, in px; 0 while none is
|
||||
* shown. CSS offsets add --view-as-offset (globals.css); offsets computed in JS add this.
|
||||
*/
|
||||
export function viewAsBarHeight(): number {
|
||||
return document.querySelector<HTMLElement>("[data-view-as-bar]")?.offsetHeight ?? 0;
|
||||
}
|
||||
|
||||
function clearViewAs(): void {
|
||||
cached = null;
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user