feat: close provider promotion and support access gates
Build and Test Quality Gate / Contract Web typecheck and build (pull_request) Successful in 3m58s
Build and Test Quality Gate / Python tests and Contract API migration (pull_request) Failing after 29m6s
Security Quality Gate / npm audit, Bandit, pip-audit, detect-secrets (pull_request) Successful in 15m55s
Build and Test Quality Gate / Contract Web typecheck and build (pull_request) Successful in 3m58s
Build and Test Quality Gate / Python tests and Contract API migration (pull_request) Failing after 29m6s
Security Quality Gate / npm audit, Bandit, pip-audit, detect-secrets (pull_request) Successful in 15m55s
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
<!-- BEGIN:nextjs-agent-rules -->
|
||||
|
||||
# This is NOT the Next.js you know
|
||||
|
||||
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.
|
||||
|
||||
This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.
|
||||
|
||||
<!-- END:nextjs-agent-rules -->
|
||||
@@ -0,0 +1 @@
|
||||
@AGENTS.md
|
||||
@@ -14,7 +14,7 @@
|
||||
"dependencies": {
|
||||
"@yuqei/design-system": "0.1.0",
|
||||
"lucide-react": "1.21.0",
|
||||
"next": "16.2.9",
|
||||
"next": "16.3.0",
|
||||
"react": "19.2.7",
|
||||
"react-dom": "19.2.7"
|
||||
},
|
||||
|
||||
@@ -6351,3 +6351,128 @@ body {
|
||||
min-width: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-nav-toggle,
|
||||
.mobile-nav-close,
|
||||
.mobile-sidebar-actions,
|
||||
.sidebar-scrim {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.app-shell {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.app-sidebar {
|
||||
position: fixed;
|
||||
z-index: 60;
|
||||
inset: 0 auto 0 0;
|
||||
width: min(86vw, 320px);
|
||||
height: 100dvh;
|
||||
max-height: 100dvh;
|
||||
transform: translateX(-105%);
|
||||
visibility: hidden;
|
||||
transition:
|
||||
transform 180ms ease,
|
||||
visibility 0s linear 180ms;
|
||||
}
|
||||
|
||||
.app-sidebar-open {
|
||||
transform: translateX(0);
|
||||
visibility: visible;
|
||||
transition-delay: 0s;
|
||||
}
|
||||
|
||||
.sidebar-scrim-visible {
|
||||
position: fixed;
|
||||
z-index: 50;
|
||||
inset: 0;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: rgba(13, 20, 18, 0.58);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.mobile-nav-toggle,
|
||||
.mobile-nav-close {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
padding: 0;
|
||||
border: 1px solid rgba(53, 67, 62, 0.18);
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 253, 248, 0.9);
|
||||
color: var(--ink);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mobile-nav-toggle {
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.mobile-nav-close {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 14px;
|
||||
right: 14px;
|
||||
display: inline-flex;
|
||||
color: #fffdf8;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border-color: rgba(255, 255, 255, 0.14);
|
||||
}
|
||||
|
||||
.app-sidebar .brand-block {
|
||||
padding-right: 52px;
|
||||
}
|
||||
|
||||
.mobile-sidebar-actions {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin-top: 4px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.mobile-sidebar-actions .workspace-switcher,
|
||||
.mobile-sidebar-actions .workspace-switcher-stack,
|
||||
.mobile-sidebar-actions .button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.app-sidebar {
|
||||
gap: 10px;
|
||||
height: 100dvh;
|
||||
max-height: 100dvh;
|
||||
}
|
||||
|
||||
.app-sidebar .nav-group {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 6px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow-x: visible;
|
||||
}
|
||||
|
||||
.app-sidebar .nav-link {
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
padding: 7px 11px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.app-sidebar {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { createContext, useContext, useEffect, type ReactNode } from "react";
|
||||
import { createContext, useContext, useEffect, useState, type ReactNode } from "react";
|
||||
import Link from "next/link";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import {
|
||||
@@ -14,12 +14,14 @@ import {
|
||||
LayoutDashboard,
|
||||
LibraryBig,
|
||||
LogOut,
|
||||
Menu,
|
||||
MessageSquareText,
|
||||
Scale,
|
||||
Search,
|
||||
Settings,
|
||||
ShieldCheck,
|
||||
UsersRound
|
||||
UsersRound,
|
||||
X
|
||||
} from "lucide-react";
|
||||
import { canAccessAdminPath, getRoleLabel, normalizeUserRole } from "@/lib/access-control";
|
||||
import type { ContractWebSession } from "@/lib/contract-session";
|
||||
@@ -75,6 +77,7 @@ const policyLinks = [
|
||||
export function AppShell({ children, session }: { children: ReactNode; session: ContractWebSession | null }) {
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
const [isMobileNavOpen, setIsMobileNavOpen] = useState(false);
|
||||
const currentRole = normalizeUserRole(session?.role);
|
||||
const visibleAssetNav = assetNav.filter(
|
||||
(item) => !item.restricted
|
||||
@@ -104,6 +107,28 @@ export function AppShell({ children, session }: { children: ReactNode; session:
|
||||
return () => window.clearTimeout(prefetchTimer);
|
||||
}, [pathname, router, session]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsMobileNavOpen(false);
|
||||
}, [pathname]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isMobileNavOpen) {
|
||||
return;
|
||||
}
|
||||
const previousOverflow = document.body.style.overflow;
|
||||
const closeOnEscape = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") {
|
||||
setIsMobileNavOpen(false);
|
||||
}
|
||||
};
|
||||
document.body.style.overflow = "hidden";
|
||||
window.addEventListener("keydown", closeOnEscape);
|
||||
return () => {
|
||||
document.body.style.overflow = previousOverflow;
|
||||
window.removeEventListener("keydown", closeOnEscape);
|
||||
};
|
||||
}, [isMobileNavOpen]);
|
||||
|
||||
async function logout() {
|
||||
await fetch("/api/auth/logout", { method: "POST" }).catch(() => null);
|
||||
router.push("/login");
|
||||
@@ -137,7 +162,26 @@ export function AppShell({ children, session }: { children: ReactNode; session:
|
||||
return (
|
||||
<ContractSessionContext.Provider value={session}>
|
||||
<div className="app-shell">
|
||||
<aside className="app-sidebar">
|
||||
<button
|
||||
aria-label="关闭导航"
|
||||
className={`sidebar-scrim ${isMobileNavOpen ? "sidebar-scrim-visible" : ""}`}
|
||||
onClick={() => setIsMobileNavOpen(false)}
|
||||
type="button"
|
||||
/>
|
||||
<aside
|
||||
aria-label="主导航"
|
||||
className={`app-sidebar ${isMobileNavOpen ? "app-sidebar-open" : ""}`}
|
||||
id="app-sidebar"
|
||||
>
|
||||
<button
|
||||
aria-label="关闭导航"
|
||||
className="mobile-nav-close"
|
||||
onClick={() => setIsMobileNavOpen(false)}
|
||||
title="关闭导航"
|
||||
type="button"
|
||||
>
|
||||
<X size={20} />
|
||||
</button>
|
||||
<Link className="brand-block" href="/workspace">
|
||||
<span className="brand-mark" aria-hidden="true">YQ</span>
|
||||
<span className="brand-copy">
|
||||
@@ -157,6 +201,14 @@ export function AppShell({ children, session }: { children: ReactNode; session:
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="mobile-sidebar-actions">
|
||||
<WorkspaceSwitcher session={session} />
|
||||
<button className="button button-soft" onClick={() => void logout()} type="button">
|
||||
<LogOut size={16} />
|
||||
退出
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="app-sidebar-footer">
|
||||
<span>{getRoleLabel(currentRole)}</span>
|
||||
<strong>AI 审核 / 留痕 / 归档</strong>
|
||||
@@ -166,6 +218,17 @@ export function AppShell({ children, session }: { children: ReactNode; session:
|
||||
|
||||
<main className="app-main">
|
||||
<header className="topbar">
|
||||
<button
|
||||
aria-controls="app-sidebar"
|
||||
aria-expanded={isMobileNavOpen}
|
||||
aria-label="打开导航"
|
||||
className="mobile-nav-toggle"
|
||||
onClick={() => setIsMobileNavOpen(true)}
|
||||
title="打开导航"
|
||||
type="button"
|
||||
>
|
||||
<Menu size={20} />
|
||||
</button>
|
||||
<div className="topbar-heading">
|
||||
<span className="topbar-kicker">流程运营中枢</span>
|
||||
<div className="topbar-title">{resolveTitle(pathname)}</div>
|
||||
|
||||
Reference in New Issue
Block a user