f21fa5b5e8
Commilitia Drop:自托管的跨设备剪贴板同步与点对点文件传输。 - 后端 Go(chi / SQLite WAL / SSE Hub / WebRTC signaling + 状态机 / Relay ring buffer),编译进单个 distroless 镜像(前端 go:embed)。 - 前端 React + TanStack Router + Zustand,自实现 SSE + WebRTC P2P,NAT 受阻时回退服务端中继;聚珍(Juzhen)CJK 综合排版。 - 桌面端 Wails v2(macOS / Windows),瘦客户端复用 web。 - 鉴权 OIDC PKCE(自建 Casdoor 等),refresh_token 信封加密存系统密钥库;iOS Shortcut 用 HS256 scoped token。 架构文档与变更记录见 docs 分支(PROJECT_BRIEF / FRONTEND_DESIGN / CHANGELOG)。 本次为公开发布初始提交:完整开发历史(含部署细节)留存于私有归档,公开仓库自此干净起步。
84 lines
3.5 KiB
TypeScript
84 lines
3.5 KiB
TypeScript
import type { ReactNode } from "react";
|
|
import { IconButton, Tooltip } from "../../ui/primitives";
|
|
import { Wordmark } from "../../ui/brand";
|
|
import { Monitor, Moon, Sun } from "lucide-react";
|
|
import { useAppStore, type ThemeMode } from "../../store";
|
|
import { t } from "../../i18n";
|
|
import s from "./AuthShell.module.css";
|
|
|
|
export interface AuthShellProps
|
|
{
|
|
title: ReactNode;
|
|
subtitle?: ReactNode;
|
|
children: ReactNode;
|
|
footer?: ReactNode;
|
|
/** 卡内品牌字标。默认不渲染——顶栏已有 logo,卡内再放会一屏两 logo;
|
|
* 需要时显式传入一个节点即可。 */
|
|
cardBrand?: ReactNode | null;
|
|
/** 隐藏顶栏 logo。过渡型认证屏(OAuth 回调 / 首次设置)用:它们会 SPA
|
|
* 跳转到主界面,跳转帧里顶栏 logo 会与主界面 header 的 logo 重叠成“两个
|
|
* logo”,隐藏即可消除。登录页是整页跳 IdP、无此重叠,保留 logo。 */
|
|
hideBrand?: boolean;
|
|
}
|
|
|
|
// 全屏外壳:登录、首次设置、OAuth 回调共用。带轻量品牌 + 主题切换,
|
|
// 让用户在登录前也能调主题。
|
|
export function AuthShell(props: AuthShellProps)
|
|
{
|
|
const { title, subtitle, children, footer, cardBrand, hideBrand } = props;
|
|
const theme = useAppStore((s) => s.theme);
|
|
const setTheme = useAppStore((s) => s.setTheme);
|
|
const next: ThemeMode = theme === "system" ? "light" : theme === "light" ? "dark" : "system";
|
|
const icon = theme === "light" ? <Sun size={16} />
|
|
: theme === "dark" ? <Moon size={16} />
|
|
: <Monitor size={16} />;
|
|
|
|
// 默认不在卡内重复 logo(顶栏已有),避免一屏两 logo;需要时显式传入。
|
|
const brandNode = cardBrand === undefined ? null : cardBrand;
|
|
|
|
return (
|
|
<div className={s.shell}>
|
|
<div className={s.topbar}>
|
|
{/* 空 span 占位保持 space-between,主题切换仍靠右。 */}
|
|
{hideBrand ? <span /> : <span className={s.brand}><Wordmark /></span>}
|
|
<Tooltip label={t("nav.themeToggle")}>
|
|
<IconButton aria-label={t("nav.themeToggle")} size="sm" onClick={() => setTheme(next)}>
|
|
{icon}
|
|
</IconButton>
|
|
</Tooltip>
|
|
</div>
|
|
|
|
<div className={s.center}>
|
|
<div className={s.card}>
|
|
{brandNode && <div className={s.cardBrand}>{brandNode}</div>}
|
|
<div className={s.hero}>
|
|
<h1 className={s.title}>{title}</h1>
|
|
{subtitle && <p className={s.subtitle} data-jz-level="paragraph">{subtitle}</p>}
|
|
</div>
|
|
<div className={s.body}>
|
|
{children}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{footer && <div className={s.footer}>{footer}</div>}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// 公共 footer 链接组件:登录页用,setup / oauth 不一定要。
|
|
export function AuthFooterLinks(props: { items: { label: string; href: string }[] })
|
|
{
|
|
const { items } = props;
|
|
return (
|
|
<div className={s.footerLinks}>
|
|
{items.map((it, i) => (
|
|
<span key={it.href} style={{ display: "inline-flex", gap: "var(--space-3)", alignItems: "center" }}>
|
|
{i > 0 && <span className={s.sep}>·</span>}
|
|
<a href={it.href} target="_blank" rel="noreferrer noopener">{it.label}</a>
|
|
</span>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|