feat: integrate pixel teacher presence and resilient classroom preview
This commit is contained in:
1 parent
12d800ec51
commit
7951cca700
60 files changed
+3313
-180
No files matched your search
@@ -0,0 +1,54 @@
|
||||
import { useRef } from 'react';
|
||||
import { useSettingsStore } from '@/stores/settings';
|
||||
import {
|
||||
CLASSROOM_SIDEBAR_DEFAULT_WIDTH,
|
||||
CLASSROOM_SIDEBAR_MIN_WIDTH,
|
||||
CLASSROOM_SIDEBAR_MAX_WIDTH,
|
||||
normalizeClassroomSidebarWidth,
|
||||
} from '@/lib/classroom-layout';
|
||||
|
||||
export function ClassroomSidebarResizer() {
|
||||
const width = useSettingsStore((state) => normalizeClassroomSidebarWidth(state.classroomSidebarWidth));
|
||||
const setWidth = useSettingsStore((state) => state.setClassroomSidebarWidth);
|
||||
const drag = useRef<{ pointerId: number; x: number; width: number } | null>(null);
|
||||
return <div
|
||||
role="separator"
|
||||
aria-label="调整左侧功能栏宽度"
|
||||
aria-orientation="vertical"
|
||||
aria-controls="classroom-sidebar"
|
||||
aria-valuemin={CLASSROOM_SIDEBAR_MIN_WIDTH}
|
||||
aria-valuemax={CLASSROOM_SIDEBAR_MAX_WIDTH}
|
||||
aria-valuenow={width}
|
||||
aria-valuetext={`${width} 像素`}
|
||||
title="拖动调整宽度,双击恢复默认"
|
||||
tabIndex={0}
|
||||
className="no-drag absolute inset-y-0 right-0 z-10 w-1.5 cursor-col-resize touch-none select-none transition-colors hover:bg-[#82986755] focus-visible:bg-[#82986755] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[#71845f]"
|
||||
onDoubleClick={() => setWidth(CLASSROOM_SIDEBAR_DEFAULT_WIDTH)}
|
||||
onPointerDown={(event) => {
|
||||
if (event.button !== 0) return;
|
||||
event.preventDefault();
|
||||
event.currentTarget.setPointerCapture(event.pointerId);
|
||||
drag.current = { pointerId: event.pointerId, x: event.clientX, width };
|
||||
}}
|
||||
onPointerMove={(event) => {
|
||||
if (drag.current?.pointerId !== event.pointerId) return;
|
||||
setWidth(drag.current.width + event.clientX - drag.current.x);
|
||||
}}
|
||||
onPointerUp={(event) => {
|
||||
if (drag.current?.pointerId !== event.pointerId) return;
|
||||
drag.current = null;
|
||||
event.currentTarget.releasePointerCapture(event.pointerId);
|
||||
}}
|
||||
onLostPointerCapture={() => { drag.current = null; }}
|
||||
onPointerCancel={() => { drag.current = null; }}
|
||||
onKeyDown={(event) => {
|
||||
const next = event.key === 'ArrowRight' ? width + 8
|
||||
: event.key === 'ArrowLeft' ? width - 8
|
||||
: event.key === 'Home' ? CLASSROOM_SIDEBAR_MIN_WIDTH
|
||||
: event.key === 'End' ? CLASSROOM_SIDEBAR_MAX_WIDTH : null;
|
||||
if (next === null) return;
|
||||
event.preventDefault();
|
||||
setWidth(next);
|
||||
}}
|
||||
/>;
|
||||
}
|
||||
@@ -40,6 +40,8 @@ import type { SidebarPeekSource } from './sidebar-peek';
|
||||
import { ModuleSwitcher } from './ModuleSwitcher';
|
||||
import { ImageWorkspaceSidebar } from './ImageWorkspaceSidebar';
|
||||
import { SidebarUpdateButton } from './SidebarUpdateButton';
|
||||
import { ClassroomSidebarResizer } from './ClassroomSidebarResizer';
|
||||
import { normalizeClassroomSidebarWidth } from '@/lib/classroom-layout';
|
||||
import { UserAvatar } from '@/components/profile/UserAvatar';
|
||||
import { getAccountInitial } from '@/components/profile/user-avatar-utils';
|
||||
import { UserProfileDialog } from '@/components/profile/UserProfileDialog';
|
||||
@@ -86,6 +88,7 @@ export function Sidebar({
|
||||
}: SidebarProps = {}) {
|
||||
const { t } = useTranslation('common');
|
||||
const storedSidebarCollapsed = useSettingsStore((state) => state.sidebarCollapsed);
|
||||
const sidebarWidth = useSettingsStore((state) => normalizeClassroomSidebarWidth(state.classroomSidebarWidth));
|
||||
const sidebarPinnedCollapsed = !forceExpanded && storedSidebarCollapsed;
|
||||
const sidebarCollapsed = sidebarPinnedCollapsed && !sidebarPeekOpen;
|
||||
const projects = useCodingWorkspaceStore((state) => state.projects);
|
||||
@@ -374,26 +377,30 @@ export function Sidebar({
|
||||
|
||||
return (
|
||||
<aside
|
||||
id={workspaceLayout ? 'classroom-sidebar' : undefined}
|
||||
data-testid="sidebar"
|
||||
data-classroom={workspaceLayout}
|
||||
aria-hidden={sidebarCollapsed}
|
||||
onPointerEnter={() => onSidebarPeekChange?.(true, 'sidebar')}
|
||||
onPointerLeave={() => onSidebarPeekChange?.(false, 'sidebar')}
|
||||
style={workspaceLayout && !sidebarCollapsed ? { width: sidebarWidth, minWidth: sidebarWidth, flexBasis: sidebarWidth } : undefined}
|
||||
className={cn(
|
||||
'sidebar-glass-surface flex min-h-0 shrink-0 flex-col overflow-hidden border-r border-border/80 text-foreground',
|
||||
sidebarCollapsed
|
||||
? 'pointer-events-none w-0 border-r-0 opacity-0'
|
||||
: workspaceLayout
|
||||
? 'w-[256px] min-w-[256px] basis-[256px] opacity-100'
|
||||
? 'opacity-100'
|
||||
: 'w-64 opacity-100',
|
||||
// Keep the collapsed preview in the same fixed layer while it opens
|
||||
// and closes. This makes the hover state cover the titlebar and the
|
||||
// full window height without reflowing the page underneath it.
|
||||
sidebarPinnedCollapsed && 'fixed inset-y-0 left-0 z-[350]',
|
||||
workspaceLayout && !sidebarPinnedCollapsed && 'relative',
|
||||
sidebarPeekPreview && 'sidebar-peek-surface pt-10 shadow-float',
|
||||
hideOnCompact && 'hidden lg:flex',
|
||||
)}
|
||||
>
|
||||
{workspaceLayout && !sidebarPinnedCollapsed && <ClassroomSidebarResizer />}
|
||||
<div
|
||||
className={cn(
|
||||
'bg-transparent px-3 text-foreground',
|
||||
@@ -402,8 +409,8 @@ export function Sidebar({
|
||||
)}
|
||||
>
|
||||
<div className="min-w-0 w-full">
|
||||
{workspaceLayout ? <button type="button" aria-label="返回首页" title="返回首页" onClick={() => navigate('/module-select')} className="flex w-full items-center gap-2.5 rounded-lg px-3 pb-4 pt-3 text-left hover:bg-black/[0.025]">
|
||||
<img src={classroomMark} alt="" className="h-7 w-7" /><span className="text-lg font-semibold tracking-tight">麦洛<span className="ml-2 text-xs font-normal text-muted-foreground">Makelore</span></span>
|
||||
{workspaceLayout ? <button type="button" aria-label="返回首页" title="返回首页" onClick={() => navigate('/module-select')} className="flex w-full items-center gap-2 rounded-lg px-1 pb-4 pt-3 text-left hover:bg-black/[0.025]">
|
||||
<img src={classroomMark} alt="" className="h-7 w-7" /><span className="text-base font-semibold tracking-tight">麦洛</span>
|
||||
</button> : <ModuleSwitcher sidebarCollapsed={sidebarCollapsed} compact={!sidebarCollapsed} />}
|
||||
</div>
|
||||
</div>
|
||||
@@ -459,6 +466,7 @@ export function Sidebar({
|
||||
type="button"
|
||||
data-testid={`sidebar-course-project-${project.id}`}
|
||||
aria-label={`进入项目 ${project.name}`}
|
||||
title={project.name}
|
||||
aria-current={active ? 'page' : undefined}
|
||||
onClick={() => void openProject(project)}
|
||||
className={cn(
|
||||
@@ -471,7 +479,7 @@ export function Sidebar({
|
||||
<FolderKanban className="h-3.5 w-3.5 shrink-0 text-muted-foreground" /><span className="min-w-0 flex-1 truncate text-[13px] font-medium">{project.name}</span>
|
||||
<ChevronRight aria-hidden="true" className="h-3.5 w-3.5 shrink-0 -translate-x-0.5 text-muted-foreground opacity-50 transition-[opacity,transform,color] duration-200 ease-out group-hover/project:translate-x-0 group-hover/project:text-foreground group-hover/project:opacity-100 group-focus-visible/project:translate-x-0 group-focus-visible/project:text-foreground group-focus-visible/project:opacity-100" />
|
||||
</button>
|
||||
{active && <CodingProjectConversations key={project.id} projectId={project.id} />}
|
||||
{active && <CodingProjectConversations key={project.id} projectId={project.id} compact={workspaceLayout} />}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { invokeIpc } from '@/lib/api-client';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useCodingWorkspaceStore } from '@/stores/coding-workspace';
|
||||
import { useSettingsStore } from '@/stores/settings';
|
||||
import { normalizeClassroomSidebarWidth } from '@/lib/classroom-layout';
|
||||
import logoWordmarkSource from '@/assets/makelore-wordmark-source.png';
|
||||
import type { SidebarPeekSource } from './sidebar-peek';
|
||||
import { TITLEBAR_LOGO_WIDTH, WINDOWS_TITLEBAR_CONTROLS_WIDTH } from './titlebar-metrics';
|
||||
@@ -67,6 +68,7 @@ function ProductTitleBar({
|
||||
onSidebarPeekChange?: SidebarPeekChange;
|
||||
}) {
|
||||
const sidebarCollapsed = useSettingsStore((state) => state.sidebarCollapsed);
|
||||
const sidebarWidth = useSettingsStore((state) => normalizeClassroomSidebarWidth(state.classroomSidebarWidth));
|
||||
const activeProject = useCodingWorkspaceStore((state) => state.activeProject);
|
||||
const toolbarButtonClass = 'no-drag motion-press flex h-8 w-8 shrink-0 items-center justify-center rounded-md text-muted-foreground hover:bg-surface-subtle hover:text-foreground';
|
||||
const setSidebarCollapsed = useSettingsStore((state) => state.setSidebarCollapsed);
|
||||
@@ -153,6 +155,7 @@ function ProductTitleBar({
|
||||
<>
|
||||
<div
|
||||
data-testid="titlebar-sidebar-surface"
|
||||
style={workspaceLayout && !sidebarTitlebarHidden ? { width: sidebarWidth, minWidth: sidebarWidth, flexBasis: sidebarWidth } : undefined}
|
||||
className={cn(
|
||||
'flex h-full shrink-0 items-center gap-1 px-2',
|
||||
sidebarTitlebarHidden
|
||||
@@ -160,7 +163,7 @@ function ProductTitleBar({
|
||||
? 'w-[132px]'
|
||||
: 'w-[52px]'
|
||||
: workspaceLayout
|
||||
? 'w-[256px] min-w-[256px] basis-[256px]'
|
||||
? ''
|
||||
: 'w-64',
|
||||
overlay
|
||||
? 'pointer-events-none border-r-0 bg-transparent'
|
||||
|
||||
Reference in new issue
Block a user