음성 재생과 운영 배포 정리
This commit is contained in:
parent
8ed185ce6c
commit
ac7db95542
1020 changed files with 46863 additions and 2175 deletions
|
|
@ -35,7 +35,14 @@ const VARIANT_FOR_EXPRESSION: Record<AvatarExpression, RasterVariant> = {
|
|||
};
|
||||
|
||||
const LIVE2D_PARTS_ART_SETS = new Set(["seoyeon-live2d-v3"]);
|
||||
const PSB_GROUP_ART_SETS = new Set(["seoyeon-live2d-psb"]);
|
||||
const PERSONA_GENERATED_ART_SETS = [
|
||||
"p4-live2d-generated",
|
||||
"p5-live2d-generated",
|
||||
"p6-live2d-generated",
|
||||
"p7-live2d-generated",
|
||||
];
|
||||
const PSB_GROUP_ART_SETS = new Set(["seoyeon-live2d-psb", "seoyeon-live2d-psd-v2", ...PERSONA_GENERATED_ART_SETS]);
|
||||
const PSB_CRYING_ART_SETS = new Set(["seoyeon-live2d-psd-v2", ...PERSONA_GENERATED_ART_SETS]);
|
||||
|
||||
export function rasterVariantFor(expression: AvatarExpression): RasterVariant {
|
||||
return VARIANT_FOR_EXPRESSION[expression];
|
||||
|
|
@ -100,6 +107,13 @@ function mouthForVariant(variant: RasterVariant): string {
|
|||
return "mouth-neutral";
|
||||
}
|
||||
|
||||
function blushOpacity(artSet: string, variant: RasterVariant): number {
|
||||
if (artSet === "seoyeon-live2d-psd-v2") {
|
||||
return variant === "warm" ? 0.62 : 0.34;
|
||||
}
|
||||
return variant === "warm" ? 1 : 0.72;
|
||||
}
|
||||
|
||||
function browPoseForVariant(variant: RasterVariant): { y: number; leftRotate: number; rightRotate: number } {
|
||||
switch (variant) {
|
||||
case "sad":
|
||||
|
|
@ -255,6 +269,7 @@ export function RasterBust({
|
|||
|
||||
if (PSB_GROUP_ART_SETS.has(artSet)) {
|
||||
const part = (name: string) => `${import.meta.env.BASE_URL}avatar/${artSet}/parts/${name}.png`;
|
||||
const cryingArtSet = PSB_CRYING_ART_SETS.has(artSet);
|
||||
const speakingOpen = speaking ? clamp01(Math.max(mouth, 0.1)) : 0;
|
||||
const wideOpen = clamp01((speakingOpen - 0.34) / 0.5);
|
||||
const smallOpen = speakingOpen > 0 ? 1 - wideOpen : 0;
|
||||
|
|
@ -275,6 +290,10 @@ export function RasterBust({
|
|||
const lowerLidY = eyePose.lowerY - closedEyes * 2.4;
|
||||
const lashY = eyePose.lashY + closedEyes * 2.1;
|
||||
const expressionMouth = psbMouthForVariant(variant);
|
||||
const useSadCryingParts = cryingArtSet && variant === "sad";
|
||||
const browLeftPart = useSadCryingParts ? "brow-sad-left" : "brow-left";
|
||||
const browRightPart = useSadCryingParts ? "brow-sad-right" : "brow-right";
|
||||
const tearOpacity = useSadCryingParts ? clamp01(0.82 + openEyes * 0.18 - closedEyes * 0.35) : 0;
|
||||
const eyeDetailLayers: Array<{ name: string; mask: string; zIndex: number }> = [
|
||||
{ name: "iris-left", mask: "eye-white-left", zIndex: 9 },
|
||||
{ name: "iris-right", mask: "eye-white-right", zIndex: 9 },
|
||||
|
|
@ -329,8 +348,8 @@ export function RasterBust({
|
|||
{renderLayer("ear-left", "vg-raster__layer--ear", 5)}
|
||||
{renderLayer("ear-right", "vg-raster__layer--ear", 5)}
|
||||
{renderLayer("face-base", "vg-raster__layer--face", 6)}
|
||||
{renderLayer("blush-left", "vg-raster__layer--face", 7, { opacity: variant === "warm" ? 1 : 0.72 })}
|
||||
{renderLayer("blush-right", "vg-raster__layer--face", 7, { opacity: variant === "warm" ? 1 : 0.72 })}
|
||||
{renderLayer("blush-left", "vg-raster__layer--face", 7, { opacity: blushOpacity(artSet, variant) })}
|
||||
{renderLayer("blush-right", "vg-raster__layer--face", 7, { opacity: blushOpacity(artSet, variant) })}
|
||||
{generatedEyeVariant ? (
|
||||
<>
|
||||
{renderLayer(`eyegen-${generatedEyeVariant}-eye-left`, "vg-raster__layer--eyes", 8, {
|
||||
|
|
@ -419,13 +438,13 @@ export function RasterBust({
|
|||
transform: layerTransform(`translateY(${lowerLidY.toFixed(2)}px)`),
|
||||
transformOrigin: "60.8% 32.4%",
|
||||
})}
|
||||
{renderLayer("brow-left", "vg-raster__layer--brow", 14, {
|
||||
{renderLayer(browLeftPart, "vg-raster__layer--brow", 14, {
|
||||
transform: layerTransform(
|
||||
`translateY(${(brow.y + browFloat - talkBounce * 0.28).toFixed(2)}px) rotate(${(brow.leftRotate - speakingOpen * 0.35 - browFloat * 0.08).toFixed(2)}deg)`,
|
||||
),
|
||||
transformOrigin: "40% 27%",
|
||||
})}
|
||||
{renderLayer("brow-right", "vg-raster__layer--brow", 14, {
|
||||
{renderLayer(browRightPart, "vg-raster__layer--brow", 14, {
|
||||
transform: layerTransform(
|
||||
`translateY(${(brow.y + browFloat - talkBounce * 0.28).toFixed(2)}px) rotate(${(brow.rightRotate + speakingOpen * 0.35 + browFloat * 0.08).toFixed(2)}deg)`,
|
||||
),
|
||||
|
|
@ -433,6 +452,20 @@ export function RasterBust({
|
|||
})}
|
||||
</>
|
||||
)}
|
||||
{cryingArtSet ? (
|
||||
<>
|
||||
{renderLayer("tear-left", "vg-raster__layer--tear", 15, {
|
||||
opacity: tearOpacity,
|
||||
filter: "drop-shadow(0 0 1.5px rgba(112, 166, 218, 0.72)) brightness(1.18) saturate(1.18)",
|
||||
transform: layerTransform(`translateY(${(closedEyes * 1.4 + breath * 0.08).toFixed(2)}px)`),
|
||||
})}
|
||||
{renderLayer("tear-right", "vg-raster__layer--tear", 15, {
|
||||
opacity: tearOpacity,
|
||||
filter: "drop-shadow(0 0 1.5px rgba(112, 166, 218, 0.72)) brightness(1.18) saturate(1.18)",
|
||||
transform: layerTransform(`translateY(${(closedEyes * 1.4 + breath * 0.08).toFixed(2)}px)`),
|
||||
})}
|
||||
</>
|
||||
) : null}
|
||||
{renderLayer("nose", "vg-raster__layer--nose", 15)}
|
||||
{renderLayer("hair-side-left-1", "vg-raster__layer--hair", 16, {
|
||||
transform: layerTransform(
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { ReactNode } from "react";
|
||||
import { useEffect, type ReactNode } from "react";
|
||||
import { Topbar } from "./Topbar";
|
||||
import { Sidebar } from "./Sidebar";
|
||||
import { useAuth } from "../../lib/auth";
|
||||
import { designRoleOf, useAuth, type Role } from "../../lib/auth";
|
||||
|
||||
export interface AppShellProps {
|
||||
children: ReactNode;
|
||||
|
|
@ -11,8 +11,12 @@ export interface AppShellProps {
|
|||
hideNav?: boolean;
|
||||
/** 메인 패딩·최대폭 제거 (풀-블리드 레이아웃) */
|
||||
bleed?: boolean;
|
||||
/** 운영형 대시보드/저작 화면용 넓은 작업폭 */
|
||||
wide?: boolean;
|
||||
/** 톱바까지 제거하는 실제 전체화면 작업 공간 */
|
||||
hideTopbar?: boolean;
|
||||
/** 현재 화면 컨텍스트에 맞춘 네비/크롬 역할 */
|
||||
navRole?: Role;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -20,16 +24,33 @@ export interface AppShellProps {
|
|||
* body[data-role] 은 AuthProvider 가 관리(여기선 셸 골격만).
|
||||
* 미인증 시에도 안전하게 렌더(네비 없이) — 가드는 라우터(RequireAuth)가 담당.
|
||||
*/
|
||||
export function AppShell({ children, contextLabel, hideNav, bleed, hideTopbar }: AppShellProps) {
|
||||
export function AppShell({ children, contextLabel, hideNav, bleed, wide, hideTopbar, navRole }: AppShellProps) {
|
||||
const { user } = useAuth();
|
||||
const showNav = !hideNav && !!user;
|
||||
const shellRole = navRole ?? user?.role;
|
||||
const mainClassName = [
|
||||
"vg-main",
|
||||
bleed ? "vg-main--bleed" : "",
|
||||
wide ? "vg-main--wide" : "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
|
||||
useEffect(() => {
|
||||
const body = document.body;
|
||||
if (shellRole) body.setAttribute("data-role", designRoleOf(shellRole));
|
||||
return () => {
|
||||
if (user) body.setAttribute("data-role", designRoleOf(user.role));
|
||||
else body.removeAttribute("data-role");
|
||||
};
|
||||
}, [shellRole, user]);
|
||||
|
||||
return (
|
||||
<div className={"vg-shell" + (hideTopbar ? " vg-shell--fullscreen" : "")}>
|
||||
{hideTopbar ? null : <Topbar contextLabel={contextLabel} />}
|
||||
<div className={"vg-shell__body" + (showNav ? "" : " vg-shell__body--bare")}>
|
||||
{showNav ? <Sidebar role={user.role} /> : null}
|
||||
<main className={"vg-main" + (bleed ? " vg-main--bleed" : "")}>
|
||||
{showNav ? <Sidebar role={shellRole ?? user.role} /> : null}
|
||||
<main className={mainClassName}>
|
||||
<div className="vg-main__inner">{children}</div>
|
||||
</main>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -16,15 +16,22 @@ export interface NavItem {
|
|||
*/
|
||||
const NAV_BY_ROLE: Record<Role, NavItem[]> = {
|
||||
learner: [
|
||||
{ to: "/learn", label: "홈", icon: "home", end: true },
|
||||
{ to: "/learn", label: "대시보드", icon: "home", end: true },
|
||||
{ to: "/learn/practice", label: "학습", icon: "session" },
|
||||
{ to: "/learn/history", label: "기록", icon: "review" },
|
||||
{ to: "/settings", label: "설정", icon: "settings" },
|
||||
],
|
||||
teacher: [
|
||||
{ to: "/teach", label: "콘솔", icon: "users", end: true },
|
||||
{ to: "/teach/personas", label: "페르소나", icon: "review" },
|
||||
{ to: "/settings", label: "설정", icon: "settings" },
|
||||
],
|
||||
admin: [
|
||||
{ to: "/admin", label: "운영", icon: "shield", end: true },
|
||||
{ to: "/admin/users", label: "사용자", icon: "users" },
|
||||
{ to: "/admin/access", label: "권한", icon: "settings" },
|
||||
{ to: "/admin/tickets", label: "티켓", icon: "review" },
|
||||
{ to: "/teach/personas", label: "페르소나", icon: "review" },
|
||||
{ to: "/settings", label: "설정", icon: "settings" },
|
||||
],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||
import { Icon } from "../ui/Icon";
|
||||
import { useAuth, roleLabel } from "../../lib/auth";
|
||||
import { canAccessRole, roleHomePath, roleLabel, useAuth, type Role } from "../../lib/auth";
|
||||
import { applyTheme, readInitialTheme, type AppTheme } from "../../lib/theme";
|
||||
|
||||
/** Vignette 워드마크 — 비네트(조리개) inline SVG. dev_dashboard 마크 계승. */
|
||||
|
|
@ -45,9 +45,15 @@ export interface TopbarProps {
|
|||
export function Topbar({ contextLabel }: TopbarProps) {
|
||||
const { user, logout } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [dark, toggleTheme] = useTheme();
|
||||
|
||||
const label = contextLabel ?? (user ? roleLabel(user.role) : null);
|
||||
const switchRoles: Role[] = user?.superAdmin
|
||||
? ["learner", "teacher", "admin"]
|
||||
: user?.adminAccess
|
||||
? ["admin"]
|
||||
: [];
|
||||
|
||||
const onLogout = async () => {
|
||||
await logout();
|
||||
|
|
@ -69,6 +75,32 @@ export function Topbar({ contextLabel }: TopbarProps) {
|
|||
<span className="vg-topbar__spacer" />
|
||||
|
||||
<div className="vg-topbar__actions">
|
||||
{user && switchRoles.length > 0 ? (
|
||||
<div className="vg-topbar__switch" aria-label="공간 전환">
|
||||
{switchRoles
|
||||
.filter((role) => canAccessRole(user, role))
|
||||
.map((role) => {
|
||||
const to = roleHomePath(role);
|
||||
const active = location.pathname === to || location.pathname.startsWith(`${to}/`);
|
||||
return (
|
||||
<Link
|
||||
key={role}
|
||||
className={"vg-topbar__switch-link" + (active ? " is-active" : "")}
|
||||
to={to}
|
||||
aria-label={roleLabel(role)}
|
||||
title={roleLabel(role)}
|
||||
>
|
||||
<Icon
|
||||
name={role === "admin" ? "shield" : role === "teacher" ? "users" : "home"}
|
||||
size={16}
|
||||
/>
|
||||
<span>{role === "admin" ? "관리자" : role === "teacher" ? "교수자" : "학습자"}</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="vg-iconbtn"
|
||||
|
|
@ -83,7 +115,7 @@ export function Topbar({ contextLabel }: TopbarProps) {
|
|||
<>
|
||||
<span className="vg-topbar__user">
|
||||
<span className="vg-topbar__avatar" aria-hidden="true">
|
||||
{initials(user.name)}
|
||||
{user.avatarUrl ? <img src={user.avatarUrl} alt="" /> : initials(user.name)}
|
||||
</span>
|
||||
<span className="vg-topbar__uname">{user.name}</span>
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
|
||||
/* ── 톱바 ── */
|
||||
.vg-topbar {
|
||||
min-width: 0;
|
||||
height: var(--topbar-h);
|
||||
background: var(--bg-surface);
|
||||
border-bottom: 1px solid var(--hair);
|
||||
|
|
@ -66,10 +67,42 @@
|
|||
flex: 1;
|
||||
}
|
||||
.vg-topbar__actions {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-2);
|
||||
}
|
||||
.vg-topbar__switch {
|
||||
min-width: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 3px;
|
||||
border: 1px solid var(--hair);
|
||||
border-radius: var(--radius);
|
||||
background: var(--bg-surface-2);
|
||||
}
|
||||
.vg-topbar__switch-link {
|
||||
min-width: 0;
|
||||
height: 28px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
padding: 0 8px;
|
||||
border-radius: calc(var(--radius) - 2px);
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
font-weight: 650;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.vg-topbar__switch-link:hover,
|
||||
.vg-topbar__switch-link.is-active {
|
||||
background: var(--bg-surface);
|
||||
color: var(--text-strong);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* 운영 콘솔은 생성 시안처럼 어두운 크롬을 쓴다. 본문 컴포넌트 토큰은 그대로 유지한다. */
|
||||
body[data-role="admin"] .vg-topbar {
|
||||
|
|
@ -88,6 +121,18 @@ body[data-role="admin"] .vg-topbar__brand svg {
|
|||
body[data-role="admin"] .vg-topbar__user {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
body[data-role="admin"] .vg-topbar__switch {
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
body[data-role="admin"] .vg-topbar__switch-link {
|
||||
color: rgba(238, 244, 242, 0.72);
|
||||
}
|
||||
body[data-role="admin"] .vg-topbar__switch-link:hover,
|
||||
body[data-role="admin"] .vg-topbar__switch-link.is-active {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #ffffff;
|
||||
}
|
||||
body[data-role="admin"] .vg-topbar__avatar {
|
||||
background: rgba(126, 184, 173, 0.18);
|
||||
color: #bfe0d9;
|
||||
|
|
@ -126,6 +171,8 @@ body[data-role="admin"] .vg-iconbtn:hover {
|
|||
|
||||
/* 사용자 칩 */
|
||||
.vg-topbar__user {
|
||||
min-width: 0;
|
||||
max-width: min(320px, 34vw);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
|
|
@ -145,8 +192,19 @@ body[data-role="admin"] .vg-iconbtn:hover {
|
|||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
flex: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
.vg-topbar__avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
}
|
||||
.vg-topbar__uname {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text-strong);
|
||||
|
|
@ -327,6 +385,9 @@ body[data-role="instructor"] .vg-nav__foot {
|
|||
max-width: var(--maxw);
|
||||
margin: 0 auto;
|
||||
}
|
||||
.vg-main--wide .vg-main__inner {
|
||||
max-width: min(1480px, 100%);
|
||||
}
|
||||
/* 풀-블리드(세션 화면 등): 패딩/최대폭 없이 셸만 */
|
||||
.vg-main--bleed {
|
||||
padding: 0;
|
||||
|
|
@ -411,6 +472,21 @@ body[data-role="instructor"] .vg-nav__foot {
|
|||
.vg-topbar__uname {
|
||||
display: none;
|
||||
}
|
||||
.vg-topbar {
|
||||
padding: 0 var(--sp-4);
|
||||
gap: var(--sp-3);
|
||||
}
|
||||
.vg-topbar__user {
|
||||
max-width: 42px;
|
||||
padding-right: 6px;
|
||||
}
|
||||
.vg-topbar__switch-link span {
|
||||
display: none;
|
||||
}
|
||||
.vg-topbar__switch-link {
|
||||
width: 28px;
|
||||
padding: 0;
|
||||
}
|
||||
.vg-main {
|
||||
padding: var(--sp-5) var(--sp-4) var(--sp-7);
|
||||
}
|
||||
|
|
@ -448,3 +524,152 @@ body[data-role="instructor"] .vg-nav__foot {
|
|||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Unified role chrome
|
||||
역할별 제품처럼 보이던 톱바/사이드바 변형을 하나의 앱 크롬으로 통합한다.
|
||||
역할 차이는 tokens.css의 accent만 사용하고, 형태·밀도·배경 체계는 동일하게 유지한다. */
|
||||
body[data-role="admin"] .vg-topbar,
|
||||
body[data-role="instructor"] .vg-topbar {
|
||||
background: var(--bg-surface);
|
||||
border-bottom-color: var(--hair);
|
||||
color: var(--text-strong);
|
||||
}
|
||||
body[data-role="admin"] .vg-topbar__wm,
|
||||
body[data-role="admin"] .vg-topbar__role,
|
||||
body[data-role="admin"] .vg-topbar__uname,
|
||||
body[data-role="admin"] .vg-iconbtn,
|
||||
body[data-role="instructor"] .vg-topbar__wm,
|
||||
body[data-role="instructor"] .vg-topbar__role,
|
||||
body[data-role="instructor"] .vg-topbar__uname,
|
||||
body[data-role="instructor"] .vg-iconbtn {
|
||||
color: var(--text-strong);
|
||||
}
|
||||
body[data-role="admin"] .vg-topbar__wm .v,
|
||||
body[data-role="instructor"] .vg-topbar__wm .v,
|
||||
body[data-role="admin"] .vg-topbar__mark,
|
||||
body[data-role="instructor"] .vg-topbar__mark,
|
||||
body[data-role="admin"] .vg-topbar__brand svg,
|
||||
body[data-role="instructor"] .vg-topbar__brand svg {
|
||||
color: var(--accent);
|
||||
}
|
||||
body[data-role="admin"] .vg-topbar__role,
|
||||
body[data-role="instructor"] .vg-topbar__role {
|
||||
border-left-color: var(--hair);
|
||||
}
|
||||
body[data-role="admin"] .vg-topbar__user,
|
||||
body[data-role="instructor"] .vg-topbar__user {
|
||||
background: var(--bg-surface-2);
|
||||
}
|
||||
body[data-role="admin"] .vg-topbar__avatar,
|
||||
body[data-role="instructor"] .vg-topbar__avatar {
|
||||
background: var(--accent-tint);
|
||||
color: var(--accent-deep);
|
||||
}
|
||||
body[data-role="admin"] .vg-iconbtn:hover,
|
||||
body[data-role="instructor"] .vg-iconbtn:hover {
|
||||
background: var(--bg-surface-2);
|
||||
color: var(--text-strong);
|
||||
}
|
||||
body[data-role="admin"] .vg-shell__body,
|
||||
body[data-role="instructor"] .vg-shell__body {
|
||||
--nav-cur: var(--nav-w);
|
||||
background-image: linear-gradient(var(--hair), var(--hair));
|
||||
}
|
||||
body[data-role="admin"] .vg-nav,
|
||||
body[data-role="instructor"] .vg-nav {
|
||||
background: var(--bg-surface);
|
||||
color: var(--text-strong);
|
||||
padding: var(--sp-5) var(--sp-3);
|
||||
}
|
||||
body[data-role="admin"] .vg-nav__label,
|
||||
body[data-role="admin"] .vg-nav__ethic,
|
||||
body[data-role="instructor"] .vg-nav__label,
|
||||
body[data-role="instructor"] .vg-nav__ethic {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
body[data-role="admin"] .vg-nav__item,
|
||||
body[data-role="instructor"] .vg-nav__item {
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
gap: 11px;
|
||||
min-height: 0;
|
||||
padding: 9px 12px;
|
||||
color: var(--text-body);
|
||||
font-size: 14px;
|
||||
text-align: left;
|
||||
}
|
||||
body[data-role="admin"] .vg-nav__item:hover,
|
||||
body[data-role="instructor"] .vg-nav__item:hover {
|
||||
background: var(--bg-surface-2);
|
||||
color: var(--text-strong);
|
||||
}
|
||||
body[data-role="admin"] .vg-nav__item .vg-nav__ic,
|
||||
body[data-role="instructor"] .vg-nav__item .vg-nav__ic {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
body[data-role="admin"] .vg-nav__item.is-active,
|
||||
body[data-role="instructor"] .vg-nav__item.is-active {
|
||||
background: var(--accent-tint);
|
||||
color: var(--accent-deep);
|
||||
}
|
||||
body[data-role="admin"] .vg-nav__item.is-active .vg-nav__ic,
|
||||
body[data-role="instructor"] .vg-nav__item.is-active .vg-nav__ic {
|
||||
color: var(--accent);
|
||||
}
|
||||
body[data-role="admin"] .vg-nav__foot,
|
||||
body[data-role="instructor"] .vg-nav__foot {
|
||||
border-top-color: var(--hair);
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
body[data-role="admin"] .vg-shell__body,
|
||||
body[data-role="instructor"] .vg-shell__body {
|
||||
--nav-cur: var(--nav-w-collapsed);
|
||||
}
|
||||
body[data-role="admin"] .vg-nav,
|
||||
body[data-role="instructor"] .vg-nav {
|
||||
padding: var(--sp-4) var(--sp-2);
|
||||
}
|
||||
body[data-role="admin"] .vg-nav__item,
|
||||
body[data-role="instructor"] .vg-nav__item {
|
||||
justify-content: center;
|
||||
padding: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
body[data-role="admin"] .vg-shell__body,
|
||||
body[data-role="instructor"] .vg-shell__body {
|
||||
display: block;
|
||||
background-image: none;
|
||||
}
|
||||
body[data-role="admin"] .vg-nav,
|
||||
body[data-role="instructor"] .vg-nav {
|
||||
position: sticky;
|
||||
top: var(--topbar-h);
|
||||
z-index: 29;
|
||||
height: 58px;
|
||||
padding: 7px max(12px, env(safe-area-inset-left)) 7px max(12px, env(safe-area-inset-right));
|
||||
border-bottom: 1px solid var(--hair);
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 8px;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
body[data-role="admin"] .vg-nav__item,
|
||||
body[data-role="instructor"] .vg-nav__item {
|
||||
min-width: 112px;
|
||||
height: 44px;
|
||||
justify-content: center;
|
||||
padding: 0 14px;
|
||||
gap: 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
body[data-role="admin"] .vg-nav__item span:not(.vg-nav__ic),
|
||||
body[data-role="instructor"] .vg-nav__item span:not(.vg-nav__ic) {
|
||||
display: inline;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ export type IconName =
|
|||
| "chevron-right"
|
||||
| "chevron-left"
|
||||
| "check"
|
||||
| "share"
|
||||
| "mic"
|
||||
| "mic-off"
|
||||
| "pause"
|
||||
|
|
@ -93,6 +94,14 @@ const PATHS: Record<IconName, ReactNode> = {
|
|||
"chevron-right": <polyline points="9 6 15 12 9 18" />,
|
||||
"chevron-left": <polyline points="15 6 9 12 15 18" />,
|
||||
check: <polyline points="20 6 9 17 4 12" />,
|
||||
share: (
|
||||
<>
|
||||
<circle cx="18" cy="5" r="3" />
|
||||
<circle cx="6" cy="12" r="3" />
|
||||
<circle cx="18" cy="19" r="3" />
|
||||
<path d="M8.6 10.5 15.4 6.5M8.6 13.5l6.8 4" />
|
||||
</>
|
||||
),
|
||||
mic: (
|
||||
<>
|
||||
<path d="M12 2a3 3 0 0 0-3 3v6a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3z" />
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue