feat: initial public release (MAESTRO v0.1.0)
Open-source release of MAESTRO, an agent orchestration platform that runs LLM-driven tasks through sandboxed tools, with a web UI. Apache-2.0. See README.md and docs/ (getting-started, configuration, architecture).
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import type { PageId } from '../../lib/urlState';
|
||||
import type { AuthUser } from '../../App';
|
||||
|
||||
interface TopBarProps {
|
||||
currentPage: PageId;
|
||||
onNavigate: (page: PageId) => void;
|
||||
isAdmin?: boolean;
|
||||
authEnabled?: boolean;
|
||||
user?: AuthUser | null;
|
||||
appName?: string;
|
||||
logoUrl?: string | null;
|
||||
onOpenDrawer: () => void;
|
||||
hamburgerButtonRef?: React.RefObject<HTMLButtonElement>;
|
||||
navDrawerOpen?: boolean;
|
||||
}
|
||||
|
||||
export const NAV_ITEMS: Array<{ id: PageId; label: string; adminOnly: boolean; requiresAuth: boolean }> = [
|
||||
{ id: 'tasks', label: 'タスク', adminOnly: false, requiresAuth: false },
|
||||
{ id: 'schedules', label: 'スケジュール', adminOnly: false, requiresAuth: false },
|
||||
{ id: 'pieces', label: 'Pieces', adminOnly: true, requiresAuth: false },
|
||||
{ id: 'captcha', label: 'CAPTCHA', adminOnly: true, requiresAuth: false },
|
||||
{ id: 'settings', label: '設定', adminOnly: false, requiresAuth: false },
|
||||
{ id: 'users', label: 'ユーザー', adminOnly: true, requiresAuth: true },
|
||||
{ id: 'help', label: 'ヘルプ', adminOnly: false, requiresAuth: false },
|
||||
{ id: 'userfolder', label: 'ユーザーフォルダ', adminOnly: false, requiresAuth: false },
|
||||
];
|
||||
|
||||
export function estimateCollapseThreshold(navCount: number): number {
|
||||
return 430 + navCount * 78 + 60;
|
||||
}
|
||||
|
||||
export function useViewportNarrow(threshold: number): boolean {
|
||||
const [narrow, setNarrow] = useState(() =>
|
||||
typeof window !== 'undefined' ? window.innerWidth < threshold : false,
|
||||
);
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
const update = () => setNarrow(window.innerWidth < threshold);
|
||||
update();
|
||||
window.addEventListener('resize', update);
|
||||
return () => window.removeEventListener('resize', update);
|
||||
}, [threshold]);
|
||||
return narrow;
|
||||
}
|
||||
|
||||
export function visibleNavItemsFor(isAdmin: boolean, authEnabled: boolean) {
|
||||
return NAV_ITEMS.filter(item => {
|
||||
if (item.adminOnly && !isAdmin) return false;
|
||||
if (item.requiresAuth && !authEnabled) return false;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
export function useCompactNav(isAdmin: boolean, authEnabled: boolean): boolean {
|
||||
const visible = visibleNavItemsFor(isAdmin, authEnabled);
|
||||
return useViewportNarrow(estimateCollapseThreshold(visible.length));
|
||||
}
|
||||
|
||||
export function TopBar({
|
||||
currentPage,
|
||||
onNavigate,
|
||||
isAdmin = true,
|
||||
authEnabled = false,
|
||||
user = null,
|
||||
appName = 'MAESTRO',
|
||||
logoUrl = null,
|
||||
onOpenDrawer,
|
||||
hamburgerButtonRef,
|
||||
navDrawerOpen = false,
|
||||
}: TopBarProps) {
|
||||
const visibleNav = visibleNavItemsFor(isAdmin, authEnabled);
|
||||
const compactMode = useViewportNarrow(estimateCollapseThreshold(visibleNav.length));
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex-shrink-0 bg-white border-b border-hairline px-4 flex items-center"
|
||||
style={{
|
||||
paddingTop: 'env(safe-area-inset-top, 0px)',
|
||||
minHeight: 'calc(48px + env(safe-area-inset-top, 0px))',
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3 flex-wrap w-full py-1.5">
|
||||
<div className="flex items-center gap-4 min-w-0 self-stretch">
|
||||
{compactMode && (
|
||||
<button
|
||||
ref={hamburgerButtonRef}
|
||||
type="button"
|
||||
onClick={onOpenDrawer}
|
||||
aria-label="メニューを開く"
|
||||
aria-expanded={navDrawerOpen}
|
||||
aria-haspopup="dialog"
|
||||
aria-controls="nav-drawer"
|
||||
className="-ml-2 flex items-center justify-center w-11 h-11 rounded-md text-slate-700 hover:bg-surface focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-ring transition-colors"
|
||||
>
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden>
|
||||
<line x1="3" y1="6" x2="21" y2="6" />
|
||||
<line x1="3" y1="12" x2="21" y2="12" />
|
||||
<line x1="3" y1="18" x2="21" y2="18" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
<img
|
||||
src={logoUrl ?? `${import.meta.env.BASE_URL}favicon.svg`}
|
||||
alt=""
|
||||
className="flex-shrink-0 h-[22px] w-auto max-w-[140px] object-contain"
|
||||
/>
|
||||
<span className="text-xs font-semibold tracking-tight text-slate-900 hidden sm:inline">
|
||||
{appName}
|
||||
</span>
|
||||
<span className="text-[10px] font-mono text-slate-400 hidden sm:inline">
|
||||
v{__APP_VERSION__}
|
||||
</span>
|
||||
|
||||
{!compactMode && (
|
||||
<nav className="flex gap-5 items-stretch -mb-[13px] ml-2" aria-label="メインナビゲーション">
|
||||
{visibleNav.map(item => {
|
||||
const active = currentPage === item.id;
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
onClick={() => onNavigate(item.id)}
|
||||
aria-current={active ? 'page' : undefined}
|
||||
className={`relative px-0.5 pb-3 text-xs border-b-2 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-ring ${
|
||||
active
|
||||
? 'font-semibold text-slate-900 border-accent'
|
||||
: 'font-medium text-slate-500 border-transparent hover:text-slate-800'
|
||||
}`}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{user && (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
{user.avatarUrl ? (
|
||||
<img
|
||||
src={user.avatarUrl}
|
||||
alt={user.name ?? user.email}
|
||||
className="w-6 h-6 rounded-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-6 h-6 rounded-full bg-surface-2 text-slate-700 flex items-center justify-center text-2xs font-semibold uppercase">
|
||||
{(user.name ?? user.email).charAt(0)}
|
||||
</div>
|
||||
)}
|
||||
<span className="text-xs text-slate-600 hidden md:inline max-w-[120px] truncate">
|
||||
{user.name ?? user.email}
|
||||
</span>
|
||||
</div>
|
||||
<a
|
||||
href="/auth/logout"
|
||||
className="px-2 py-1 rounded-md text-2xs text-slate-500 hover:text-slate-800 hover:bg-surface transition-colors"
|
||||
>
|
||||
ログアウト
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user