안전 이벤트 대시보드 보강
This commit is contained in:
parent
fe2796f05a
commit
51053af536
15 changed files with 1081 additions and 423 deletions
|
|
@ -1,5 +1,5 @@
|
|||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { AppShell } from "../components/shell/AppShell";
|
||||
import { Badge, Button, Card, Dot, Icon, Kicker } from "../components/ui";
|
||||
import {
|
||||
|
|
@ -20,6 +20,8 @@ import "./professor.css";
|
|||
|
||||
type LoadState = "loading" | "ready" | "error";
|
||||
type ProfessorView = "console" | "analysis";
|
||||
type LearnerSort = "latest" | "sessions" | "score" | "delta";
|
||||
type LearnerAnalysisTab = "trend" | "sessions" | "stages";
|
||||
|
||||
function formatDateTime(value: string | null): string {
|
||||
if (!value) return "-";
|
||||
|
|
@ -95,7 +97,9 @@ function EmptyState({ title, desc }: { title: string; desc: string }) {
|
|||
}
|
||||
export default function Professor({ view = "console" }: { view?: ProfessorView }) {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const isAnalysisView = view === "analysis";
|
||||
const selectedLearnerId = isAnalysisView ? searchParams.get("learner") : null;
|
||||
const [dashboard, setDashboard] = useState<TeacherDashboardResponse | null>(null);
|
||||
const [personaReviews, setPersonaReviews] = useState<PersonaReviewSummary[]>([]);
|
||||
const [loadState, setLoadState] = useState<LoadState>("loading");
|
||||
|
|
@ -104,7 +108,8 @@ export default function Professor({ view = "console" }: { view?: ProfessorView }
|
|||
const [personaReviewError, setPersonaReviewError] = useState<string | null>(null);
|
||||
const [personaDecisionBusy, setPersonaDecisionBusy] = useState<string | null>(null);
|
||||
const [updatedAt, setUpdatedAt] = useState<Date | null>(null);
|
||||
const [selectedLearnerId, setSelectedLearnerId] = useState<string | null>(null);
|
||||
const [learnerSearch, setLearnerSearch] = useState("");
|
||||
const [learnerSort, setLearnerSort] = useState<LearnerSort>("latest");
|
||||
const [learnerAnalysis, setLearnerAnalysis] =
|
||||
useState<TeacherLearnerAnalysisResponse | null>(null);
|
||||
const [learnerAnalysisState, setLearnerAnalysisState] = useState<LoadState>("ready");
|
||||
|
|
@ -235,17 +240,49 @@ export default function Professor({ view = "console" }: { view?: ProfessorView }
|
|||
loadState === "loading" ||
|
||||
(!isAnalysisView && personaReviewLoading) ||
|
||||
(isAnalysisView && learnerAnalysisState === "loading");
|
||||
const openLearnerDetail = useCallback(
|
||||
(learnerId: string) => {
|
||||
setSearchParams({ learner: learnerId });
|
||||
},
|
||||
[setSearchParams],
|
||||
);
|
||||
const closeLearnerDetail = useCallback(() => {
|
||||
setSearchParams({});
|
||||
}, [setSearchParams]);
|
||||
const visibleLearners = useMemo(() => {
|
||||
const query = learnerSearch.trim().toLocaleLowerCase("ko-KR");
|
||||
const filtered = query
|
||||
? learnerGrowth.filter((learner) => {
|
||||
const haystack = [
|
||||
learner.learner_label,
|
||||
learner.learner_id,
|
||||
trendLabel(learner.trend),
|
||||
...(learner.top_techniques ?? []),
|
||||
]
|
||||
.join(" ")
|
||||
.toLocaleLowerCase("ko-KR");
|
||||
return haystack.includes(query);
|
||||
})
|
||||
: [...learnerGrowth];
|
||||
|
||||
return filtered.sort((a, b) => {
|
||||
if (learnerSort === "sessions") return b.sessions - a.sessions;
|
||||
if (learnerSort === "score") return (b.latest_score ?? -1) - (a.latest_score ?? -1);
|
||||
if (learnerSort === "delta") return (b.score_delta ?? -999) - (a.score_delta ?? -999);
|
||||
return Date.parse(b.latest_at ?? "") - Date.parse(a.latest_at ?? "");
|
||||
});
|
||||
}, [learnerGrowth, learnerSearch, learnerSort]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAnalysisView) return;
|
||||
if (learnerGrowth.length === 0) {
|
||||
if (selectedLearnerId !== null) setSelectedLearnerId(null);
|
||||
return;
|
||||
if (
|
||||
selectedLearnerId &&
|
||||
learnerGrowth.length > 0 &&
|
||||
!learnerGrowth.some((learner) => learner.learner_id === selectedLearnerId)
|
||||
) {
|
||||
setSearchParams({});
|
||||
}
|
||||
if (!selectedLearnerId || !learnerGrowth.some((learner) => learner.learner_id === selectedLearnerId)) {
|
||||
setSelectedLearnerId(learnerGrowth[0].learner_id);
|
||||
}
|
||||
}, [isAnalysisView, learnerGrowth, selectedLearnerId]);
|
||||
}, [isAnalysisView, learnerGrowth, selectedLearnerId, setSearchParams]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAnalysisView) {
|
||||
|
|
@ -293,10 +330,10 @@ export default function Professor({ view = "console" }: { view?: ProfessorView }
|
|||
<Kicker>{isAnalysisView ? "학생 분석" : "교수 콘솔"}</Kicker>
|
||||
{isAnalysisView ? (
|
||||
<>
|
||||
<h1>학습자별 추이와 전체 회기를 분리해서 봅니다.</h1>
|
||||
<h1>전체 학습자를 먼저 보고, 필요한 학습자만 상세로 들어갑니다.</h1>
|
||||
<p>
|
||||
왼쪽에서 학습자를 고르면 점수 변화, 단계 분포, 전체 회기 타임라인을 한 흐름으로
|
||||
확인하고 각 회기의 상세 검토로 들어갑니다.
|
||||
검색 테이블에서 학습자별 회기 수, 최근 점수, 변화폭, 라포와 미니 추이를 비교한 뒤
|
||||
상세 화면에서 추이·전체 회기·단계 분석 탭으로 나눠 확인합니다.
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
|
|
@ -408,103 +445,82 @@ export default function Professor({ view = "console" }: { view?: ProfessorView }
|
|||
) : null}
|
||||
|
||||
{isAnalysisView ? (
|
||||
<section className="pf-analysis-shell" aria-label="학생 분석 작업면">
|
||||
<aside className="pf-analysis-rail">
|
||||
<section className="pf-section pf-section--growth">
|
||||
<div className="pf-section__head">
|
||||
<div>
|
||||
<Kicker>학습자 목록</Kicker>
|
||||
<h2>사용자별 추이</h2>
|
||||
</div>
|
||||
<Badge tone={learnerGrowth.length > 0 ? "accent" : "neutral"}>
|
||||
{learnerGrowth.length}명
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<Card className="pf-panel pf-growth-panel">
|
||||
{learnerGrowth.length > 0 ? (
|
||||
<div className="pf-growth-list">
|
||||
{learnerGrowth.map((learner) => (
|
||||
<GrowthCard
|
||||
learner={learner}
|
||||
key={learner.learner_id}
|
||||
selected={learner.learner_id === selectedLearnerId}
|
||||
onSelect={() => setSelectedLearnerId(learner.learner_id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<EmptyState
|
||||
title={loadState === "loading" ? "성장 지표 계산 중" : "표시할 성장 이력 없음"}
|
||||
desc="학습자 회기와 턴별 평가가 쌓이면 적절성·라포·기법 사용 추이를 표시합니다."
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
</section>
|
||||
</aside>
|
||||
|
||||
<section className="pf-analysis-workbench">
|
||||
<nav className="pf-depth-strip" aria-label="학생 분석 단계">
|
||||
<span className="is-current">
|
||||
<small>1</small>
|
||||
학습자
|
||||
</span>
|
||||
<span>
|
||||
<small>2</small>
|
||||
추이
|
||||
</span>
|
||||
<span>
|
||||
<small>3</small>
|
||||
전체 회기
|
||||
</span>
|
||||
<span>
|
||||
<small>4</small>
|
||||
상세 검토
|
||||
</span>
|
||||
</nav>
|
||||
<section className="pf-section pf-section--learner-analysis">
|
||||
<div className="pf-section__head">
|
||||
<div>
|
||||
<Kicker>사용자별 분석</Kicker>
|
||||
<h2>{selectedLearner?.learner_label ?? "학습자 이력"}</h2>
|
||||
</div>
|
||||
<Badge tone={learnerAnalysis ? "accent" : "neutral"}>
|
||||
{learnerAnalysis ? `${learnerAnalysis.total_sessions}회기` : "대기"}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<Card className="pf-panel pf-analysis-panel">
|
||||
{learnerGrowth.length === 0 ? (
|
||||
<EmptyState
|
||||
title={loadState === "loading" ? "이력 계산 중" : "분석할 학습자 없음"}
|
||||
desc="실제 학습자 회기가 쌓이면 사용자별 전체 이력을 표시합니다."
|
||||
/>
|
||||
) : learnerAnalysisState === "loading" ? (
|
||||
<EmptyState
|
||||
title="학습자 이력 계산 중"
|
||||
desc="회기별 점수, 단계, 검토 상태를 불러오고 있습니다."
|
||||
/>
|
||||
) : learnerAnalysisError ? (
|
||||
<div className="pf-empty" role="alert">
|
||||
<Icon name="alert" size={18} />
|
||||
<b>학습자 분석을 불러오지 못했습니다</b>
|
||||
<span>{learnerAnalysisError}</span>
|
||||
</div>
|
||||
) : learnerAnalysis ? (
|
||||
<LearnerAnalysisPanel
|
||||
analysis={learnerAnalysis}
|
||||
onOpenSession={(sessionId) => navigate(`/teach/session/${sessionId}/review`)}
|
||||
<section className="pf-analysis-page" aria-label="학생 분석 작업면">
|
||||
{!selectedLearnerId ? (
|
||||
<LearnerOverviewTable
|
||||
learners={visibleLearners}
|
||||
totalLearners={learnerGrowth.length}
|
||||
query={learnerSearch}
|
||||
sort={learnerSort}
|
||||
loadState={loadState}
|
||||
onSearch={setLearnerSearch}
|
||||
onSort={setLearnerSort}
|
||||
onOpen={openLearnerDetail}
|
||||
/>
|
||||
) : (
|
||||
<EmptyState
|
||||
title="선택된 학습자 없음"
|
||||
desc="실제 회기 기반 사용자별 분석이 이 영역에 표시됩니다."
|
||||
/>
|
||||
<section className="pf-section pf-section--learner-analysis">
|
||||
<div className="pf-analysis-detail-head">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
leading={<Icon name="chevron-left" size={14} />}
|
||||
onClick={closeLearnerDetail}
|
||||
>
|
||||
전체 목록
|
||||
</Button>
|
||||
<nav className="pf-depth-strip" aria-label="학생 분석 깊이">
|
||||
<span>
|
||||
<small>1</small>
|
||||
검색 목록
|
||||
</span>
|
||||
<span className="is-current">
|
||||
<small>2</small>
|
||||
학습자 상세
|
||||
</span>
|
||||
<span>
|
||||
<small>3</small>
|
||||
회기 리뷰
|
||||
</span>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div className="pf-section__head">
|
||||
<div>
|
||||
<Kicker>사용자별 분석</Kicker>
|
||||
<h2>{selectedLearner?.learner_label ?? "학습자 이력"}</h2>
|
||||
</div>
|
||||
<Badge tone={learnerAnalysis ? "accent" : "neutral"}>
|
||||
{learnerAnalysis ? `${learnerAnalysis.total_sessions}회기` : "대기"}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<Card className="pf-panel pf-analysis-panel">
|
||||
{learnerAnalysisState === "loading" ? (
|
||||
<EmptyState
|
||||
title="학습자 이력 계산 중"
|
||||
desc="회기별 점수, 단계, 검토 상태를 불러오고 있습니다."
|
||||
/>
|
||||
) : learnerAnalysisError ? (
|
||||
<div className="pf-empty" role="alert">
|
||||
<Icon name="alert" size={18} />
|
||||
<b>학습자 분석을 불러오지 못했습니다</b>
|
||||
<span>{learnerAnalysisError}</span>
|
||||
</div>
|
||||
) : learnerAnalysis ? (
|
||||
<LearnerAnalysisPanel
|
||||
analysis={learnerAnalysis}
|
||||
onOpenSession={(sessionId) => navigate(`/teach/session/${sessionId}/review`)}
|
||||
/>
|
||||
) : (
|
||||
<EmptyState
|
||||
title="선택된 학습자 없음"
|
||||
desc="전체 목록에서 학습자를 선택하면 상세 분석이 열립니다."
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
</section>
|
||||
)}
|
||||
</Card>
|
||||
</section>
|
||||
</section>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{!isAnalysisView ? (
|
||||
|
|
@ -798,6 +814,150 @@ export default function Professor({ view = "console" }: { view?: ProfessorView }
|
|||
);
|
||||
}
|
||||
|
||||
function LearnerOverviewTable({
|
||||
learners,
|
||||
totalLearners,
|
||||
query,
|
||||
sort,
|
||||
loadState,
|
||||
onSearch,
|
||||
onSort,
|
||||
onOpen,
|
||||
}: {
|
||||
learners: TeacherLearnerGrowth[];
|
||||
totalLearners: number;
|
||||
query: string;
|
||||
sort: LearnerSort;
|
||||
loadState: LoadState;
|
||||
onSearch: (value: string) => void;
|
||||
onSort: (value: LearnerSort) => void;
|
||||
onOpen: (learnerId: string) => void;
|
||||
}) {
|
||||
const sortOptions: Array<{ key: LearnerSort; label: string }> = [
|
||||
{ key: "latest", label: "최근순" },
|
||||
{ key: "sessions", label: "회기순" },
|
||||
{ key: "score", label: "점수순" },
|
||||
{ key: "delta", label: "변화순" },
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="pf-section pf-section--learner-overview">
|
||||
<div className="pf-section__head">
|
||||
<div>
|
||||
<Kicker>전체 학습자</Kicker>
|
||||
<h2>검색 가능한 학습자 추이 테이블</h2>
|
||||
</div>
|
||||
<Badge tone={totalLearners > 0 ? "accent" : "neutral"}>{totalLearners}명</Badge>
|
||||
</div>
|
||||
|
||||
<Card className="pf-panel pf-learner-overview">
|
||||
<div className="pf-learner-toolbar">
|
||||
<label className="pf-search-field">
|
||||
<Icon name="users" size={15} />
|
||||
<input
|
||||
aria-label="학습자 검색"
|
||||
placeholder="학습자, ID, 기법 검색"
|
||||
value={query}
|
||||
onChange={(event) => onSearch(event.currentTarget.value)}
|
||||
/>
|
||||
</label>
|
||||
<div className="pf-sort-segment" aria-label="학습자 정렬">
|
||||
{sortOptions.map((option) => (
|
||||
<button
|
||||
type="button"
|
||||
className={sort === option.key ? "is-active" : ""}
|
||||
key={option.key}
|
||||
onClick={() => onSort(option.key)}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{totalLearners === 0 ? (
|
||||
<EmptyState
|
||||
title={loadState === "loading" ? "학습자 추이 계산 중" : "표시할 학습자 이력 없음"}
|
||||
desc="실제 학습자 회기와 턴별 평가가 쌓이면 이 표에 전체 학습자 추이가 표시됩니다."
|
||||
/>
|
||||
) : learners.length === 0 ? (
|
||||
<EmptyState
|
||||
title="검색 결과 없음"
|
||||
desc="검색어를 줄이거나 다른 학습자 이름, ID, 기법 태그로 다시 확인합니다."
|
||||
/>
|
||||
) : (
|
||||
<div className="pf-learner-table" data-learner-overview-table="true">
|
||||
<div className="pf-learner-table__head" aria-hidden="true">
|
||||
<span>학습자</span>
|
||||
<span>회기</span>
|
||||
<span>추이</span>
|
||||
<span>최근 적절성</span>
|
||||
<span>변화</span>
|
||||
<span>평균 라포</span>
|
||||
<span>기법</span>
|
||||
<span>상세</span>
|
||||
</div>
|
||||
{learners.map((learner) => (
|
||||
<article
|
||||
className="pf-learner-row"
|
||||
data-learner-overview-row="true"
|
||||
key={learner.learner_id}
|
||||
>
|
||||
<div className="pf-learner-row__id">
|
||||
<b>{learner.learner_label}</b>
|
||||
<span>{learner.learner_id}</span>
|
||||
</div>
|
||||
<div className="pf-learner-row__metric" data-label="회기">
|
||||
<b>
|
||||
{learner.ended_sessions}/{learner.sessions}
|
||||
</b>
|
||||
<span>{formatDateTime(learner.latest_at)}</span>
|
||||
</div>
|
||||
<div className="pf-learner-row__spark" data-label="추이">
|
||||
<div className="pf-learner-spark" aria-label={`${learner.learner_label} 회기별 추이`}>
|
||||
{(learner.points ?? []).map((point) => (
|
||||
<GrowthBar point={point} key={point.session_id} />
|
||||
))}
|
||||
</div>
|
||||
<Badge tone={learner.trend === "down" ? "warn" : learner.trend === "up" ? "accent" : "neutral"}>
|
||||
{trendLabel(learner.trend)}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="pf-learner-row__metric" data-label="최근 적절성">
|
||||
<b>{formatScore(learner.latest_score)}</b>
|
||||
</div>
|
||||
<div className="pf-learner-row__metric" data-label="변화">
|
||||
<b>{formatDelta(learner.score_delta)}</b>
|
||||
</div>
|
||||
<div className="pf-learner-row__metric" data-label="평균 라포">
|
||||
<b>{formatRapport(learner.avg_rapport)}</b>
|
||||
</div>
|
||||
<div className="pf-learner-row__tags" data-label="기법">
|
||||
{(learner.top_techniques ?? []).slice(0, 3).map((tag) => (
|
||||
<span key={tag}>{tag}</span>
|
||||
))}
|
||||
{(learner.top_techniques ?? []).length === 0 ? <span>기법 부족</span> : null}
|
||||
</div>
|
||||
<div className="pf-learner-row__action">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
leading={<Icon name="chevron-right" size={14} />}
|
||||
aria-label={`${learner.learner_label} 상세 분석 열기`}
|
||||
onClick={() => onOpen(learner.learner_id)}
|
||||
>
|
||||
상세 분석
|
||||
</Button>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function LearnerAnalysisPanel({
|
||||
analysis,
|
||||
onOpenSession,
|
||||
|
|
@ -805,11 +965,17 @@ function LearnerAnalysisPanel({
|
|||
analysis: TeacherLearnerAnalysisResponse;
|
||||
onOpenSession: (sessionId: string) => void;
|
||||
}) {
|
||||
const [activeTab, setActiveTab] = useState<LearnerAnalysisTab>("trend");
|
||||
const points = analysis.points ?? [];
|
||||
const sessions = analysis.sessions ?? [];
|
||||
const pointBySession = new Map(points.map((point) => [point.session_id, point]));
|
||||
const stageBreakdown = analysis.stage_breakdown ?? [];
|
||||
const summary = analysis.summary;
|
||||
const tabs: Array<{ key: LearnerAnalysisTab; label: string; count: string }> = [
|
||||
{ key: "trend", label: "추이", count: `${points.length}점` },
|
||||
{ key: "sessions", label: "전체 회기", count: `${sessions.length}건` },
|
||||
{ key: "stages", label: "단계 분석", count: `${stageBreakdown.length}단계` },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="pf-analysis" data-learner-analysis-panel="true">
|
||||
|
|
@ -848,7 +1014,23 @@ function LearnerAnalysisPanel({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pf-analysis__body">
|
||||
<div className="pf-analysis-tabs" role="tablist" aria-label="학습자 상세 분석 탭">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={activeTab === tab.key}
|
||||
className={activeTab === tab.key ? "is-active" : ""}
|
||||
key={tab.key}
|
||||
onClick={() => setActiveTab(tab.key)}
|
||||
>
|
||||
<b>{tab.label}</b>
|
||||
<span>{tab.count}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{activeTab === "trend" ? (
|
||||
<section className="pf-analysis__chart" aria-label="회기별 추이">
|
||||
<div className="pf-analysis__subhead">
|
||||
<b>회기별 추이</b>
|
||||
|
|
@ -864,7 +1046,9 @@ function LearnerAnalysisPanel({
|
|||
<EmptyState title="회기별 점수 없음" desc="평가가 저장된 턴이 아직 없습니다." />
|
||||
)}
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{activeTab === "stages" ? (
|
||||
<section className="pf-analysis__stages" aria-label="단계 분포">
|
||||
<div className="pf-analysis__subhead">
|
||||
<b>단계 분포</b>
|
||||
|
|
@ -880,28 +1064,30 @@ function LearnerAnalysisPanel({
|
|||
))}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<section className="pf-analysis__timeline" aria-label="전체 회기 타임라인">
|
||||
<div className="pf-analysis__subhead">
|
||||
<b>전체 회기</b>
|
||||
<span>{sessions.length}건</span>
|
||||
</div>
|
||||
{sessions.length > 0 ? (
|
||||
<div className="pf-timeline">
|
||||
{sessions.map((session) => (
|
||||
<LearnerSessionRow
|
||||
session={session}
|
||||
point={pointBySession.get(session.session_id)}
|
||||
key={session.session_id}
|
||||
onOpen={() => onOpenSession(session.session_id)}
|
||||
/>
|
||||
))}
|
||||
{activeTab === "sessions" ? (
|
||||
<section className="pf-analysis__timeline" aria-label="전체 회기 타임라인">
|
||||
<div className="pf-analysis__subhead">
|
||||
<b>전체 회기</b>
|
||||
<span>{sessions.length}건</span>
|
||||
</div>
|
||||
) : (
|
||||
<EmptyState title="회기 없음" desc="표시할 회기 기록이 없습니다." />
|
||||
)}
|
||||
</section>
|
||||
{sessions.length > 0 ? (
|
||||
<div className="pf-timeline">
|
||||
{sessions.map((session) => (
|
||||
<LearnerSessionRow
|
||||
session={session}
|
||||
point={pointBySession.get(session.session_id)}
|
||||
key={session.session_id}
|
||||
onOpen={() => onOpenSession(session.session_id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<EmptyState title="회기 없음" desc="표시할 회기 기록이 없습니다." />
|
||||
)}
|
||||
</section>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -962,91 +1148,6 @@ function LearnerSessionRow({
|
|||
);
|
||||
}
|
||||
|
||||
function GrowthCard({
|
||||
learner,
|
||||
selected,
|
||||
onSelect,
|
||||
}: {
|
||||
learner: TeacherLearnerGrowth;
|
||||
selected: boolean;
|
||||
onSelect: () => void;
|
||||
}) {
|
||||
const points = learner.points ?? [];
|
||||
const topTechniques = learner.top_techniques ?? [];
|
||||
const recentPoints = points.slice(-3).reverse();
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={`pf-growth-card trend-${learner.trend} ${selected ? "is-selected" : ""}`}
|
||||
data-learner-growth-card="true"
|
||||
aria-pressed={selected}
|
||||
aria-label={`${learner.learner_label} 사용자별 분석`}
|
||||
onClick={onSelect}
|
||||
>
|
||||
<div className="pf-growth-card__top">
|
||||
<div className="pf-growth-card__id">
|
||||
<b>{learner.learner_label}</b>
|
||||
<span>
|
||||
{learner.ended_sessions}/{learner.sessions}회기 완료 · {formatDateTime(learner.latest_at)}
|
||||
</span>
|
||||
</div>
|
||||
<Badge tone={learner.trend === "down" ? "warn" : learner.trend === "up" ? "accent" : "neutral"}>
|
||||
{trendLabel(learner.trend)}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="pf-growth-card__metrics" aria-label="학습자 성장 요약">
|
||||
<span>
|
||||
<small>최근 적절성</small>
|
||||
<b>{formatScore(learner.latest_score)}</b>
|
||||
</span>
|
||||
<span>
|
||||
<small>변화</small>
|
||||
<b>{formatDelta(learner.score_delta)}</b>
|
||||
</span>
|
||||
<span>
|
||||
<small>평균 라포</small>
|
||||
<b>{formatScore(learner.avg_rapport == null ? null : (learner.avg_rapport + 1) / 2)}</b>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="pf-growth-bars" aria-label="회기별 적절성 추이">
|
||||
{points.map((point) => (
|
||||
<GrowthBar point={point} key={point.session_id} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="pf-growth-card__tags">
|
||||
{topTechniques.length > 0 ? (
|
||||
topTechniques.map((tag) => <span key={tag}>{tag}</span>)
|
||||
) : (
|
||||
<span>기법 태그 부족</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="pf-growth-card__points">
|
||||
{recentPoints.length > 0 ? (
|
||||
recentPoints.map((point) => (
|
||||
<div className="pf-growth-point" key={point.session_id}>
|
||||
<b>
|
||||
{point.persona_code} · {point.session_no}회기
|
||||
</b>
|
||||
<span>
|
||||
{formatScore(point.score)} · 기법 {point.technique_count} · 점검 {point.watch_count}
|
||||
</span>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="pf-growth-point">
|
||||
<b>회기 평가 부족</b>
|
||||
<span>종료 회기와 턴별 평가가 필요합니다.</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function GrowthBar({ point }: { point: TeacherGrowthPoint }) {
|
||||
const hasScore = typeof point.score === "number" && !Number.isNaN(point.score);
|
||||
const height = hasScore ? Math.max(10, Math.round((point.score ?? 0) * 100)) : 10;
|
||||
|
|
|
|||
|
|
@ -181,17 +181,11 @@
|
|||
gap:14px;
|
||||
min-width:0;
|
||||
}
|
||||
.pf-analysis-shell{
|
||||
.pf-analysis-page{
|
||||
order:2;
|
||||
display:grid;
|
||||
grid-template-columns:minmax(300px,360px) minmax(0,1fr);
|
||||
align-items:start;
|
||||
gap:14px;
|
||||
min-width:0;
|
||||
}
|
||||
.pf-analysis-rail{
|
||||
position:sticky;
|
||||
top:16px;
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
gap:12px;
|
||||
min-width:0;
|
||||
}
|
||||
.pf-analysis-workbench{
|
||||
|
|
@ -200,9 +194,16 @@
|
|||
flex-direction:column;
|
||||
gap:10px;
|
||||
}
|
||||
.pf-analysis-detail-head{
|
||||
display:grid;
|
||||
grid-template-columns:auto minmax(0,1fr);
|
||||
gap:10px;
|
||||
align-items:center;
|
||||
min-width:0;
|
||||
}
|
||||
.pf-depth-strip{
|
||||
display:grid;
|
||||
grid-template-columns:repeat(4,minmax(0,1fr));
|
||||
grid-template-columns:repeat(3,minmax(0,1fr));
|
||||
min-width:0;
|
||||
border:1px solid var(--hair);
|
||||
border-radius:var(--radius);
|
||||
|
|
@ -325,86 +326,217 @@
|
|||
.pf-growth-panel{
|
||||
background:linear-gradient(180deg,var(--bg-surface),color-mix(in srgb,var(--accent-tint) 18%,var(--bg-surface)));
|
||||
}
|
||||
.pf-growth-list{
|
||||
max-height:min(420px,44vh);
|
||||
overflow:auto;
|
||||
scrollbar-gutter:stable;
|
||||
.pf-learner-overview{
|
||||
background:linear-gradient(180deg,var(--bg-surface),color-mix(in srgb,var(--bg-surface-2) 72%,var(--bg-surface)));
|
||||
}
|
||||
.pf-learner-toolbar{
|
||||
display:grid;
|
||||
grid-template-columns:repeat(3,minmax(0,1fr));
|
||||
gap:12px;
|
||||
grid-template-columns:minmax(240px,.72fr) auto;
|
||||
gap:10px;
|
||||
align-items:center;
|
||||
padding:12px;
|
||||
border-bottom:1px solid var(--hair);
|
||||
background:color-mix(in srgb,var(--bg-surface) 86%,var(--bg-surface-2));
|
||||
}
|
||||
.pf-analysis-rail .pf-growth-list{
|
||||
max-height:min(690px,calc(100vh - 250px));
|
||||
grid-template-columns:1fr;
|
||||
gap:9px;
|
||||
padding:10px;
|
||||
.pf-search-field{
|
||||
min-width:0;
|
||||
display:grid;
|
||||
grid-template-columns:auto minmax(0,1fr);
|
||||
gap:8px;
|
||||
align-items:center;
|
||||
padding:9px 10px;
|
||||
border:1px solid var(--paper-2);
|
||||
border-radius:var(--radius);
|
||||
color:var(--text-muted);
|
||||
background:var(--bg-surface-2);
|
||||
}
|
||||
.pf-growth-card{
|
||||
width:100%;
|
||||
.pf-search-field input{
|
||||
min-width:0;
|
||||
border:0;
|
||||
outline:0;
|
||||
color:var(--text-strong);
|
||||
background:transparent;
|
||||
font:inherit;
|
||||
text-align:left;
|
||||
color:inherit;
|
||||
font-size:var(--fs-sm);
|
||||
}
|
||||
.pf-search-field input::placeholder{
|
||||
color:var(--text-muted);
|
||||
}
|
||||
.pf-sort-segment{
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:flex-end;
|
||||
gap:0;
|
||||
border:1px solid var(--paper-2);
|
||||
border-radius:var(--radius);
|
||||
overflow:hidden;
|
||||
background:var(--bg-surface-2);
|
||||
}
|
||||
.pf-sort-segment button{
|
||||
min-height:38px;
|
||||
padding:0 11px;
|
||||
border:0;
|
||||
border-left:1px solid var(--hair);
|
||||
color:var(--text-muted);
|
||||
background:transparent;
|
||||
font:inherit;
|
||||
font-size:var(--fs-xs);
|
||||
font-weight:760;
|
||||
cursor:pointer;
|
||||
white-space:nowrap;
|
||||
}
|
||||
.pf-sort-segment button:first-child{
|
||||
border-left:0;
|
||||
}
|
||||
.pf-sort-segment button:hover,
|
||||
.pf-sort-segment button:focus-visible{
|
||||
color:var(--text-strong);
|
||||
background:var(--bg-surface);
|
||||
}
|
||||
.pf-sort-segment button:focus-visible{
|
||||
outline:2px solid var(--accent);
|
||||
outline-offset:-2px;
|
||||
}
|
||||
.pf-sort-segment button.is-active{
|
||||
color:var(--text-on-accent);
|
||||
background:var(--accent);
|
||||
}
|
||||
.pf-learner-table{
|
||||
min-width:0;
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
gap:12px;
|
||||
padding:13px;
|
||||
border:1px solid var(--hair);
|
||||
border-radius:var(--radius-sm);
|
||||
background:color-mix(in srgb,var(--bg-surface) 82%,var(--bg-surface-2));
|
||||
box-shadow:none;
|
||||
cursor:pointer;
|
||||
}
|
||||
.pf-growth-card:hover{
|
||||
border-color:var(--accent);
|
||||
background:var(--bg-surface);
|
||||
}
|
||||
.pf-growth-card:focus-visible{
|
||||
outline:2px solid var(--accent);
|
||||
outline-offset:2px;
|
||||
}
|
||||
.pf-growth-card.is-selected{
|
||||
border-color:var(--accent);
|
||||
background:color-mix(in srgb,var(--accent-tint) 34%,var(--bg-surface));
|
||||
}
|
||||
.pf-analysis-rail .pf-growth-card{
|
||||
.pf-learner-table__head,
|
||||
.pf-learner-row{
|
||||
display:grid;
|
||||
grid-template-columns:
|
||||
minmax(170px,1.25fr)
|
||||
minmax(86px,.55fr)
|
||||
minmax(164px,.9fr)
|
||||
minmax(76px,.48fr)
|
||||
minmax(64px,.42fr)
|
||||
minmax(76px,.48fr)
|
||||
minmax(118px,.75fr)
|
||||
minmax(96px,.42fr);
|
||||
gap:9px;
|
||||
padding:11px;
|
||||
border-radius:var(--radius);
|
||||
}
|
||||
.pf-analysis-rail .pf-growth-card__points{
|
||||
display:none;
|
||||
}
|
||||
.pf-analysis-rail .pf-growth-card__tags{
|
||||
min-height:0;
|
||||
}
|
||||
.pf-analysis-rail .pf-growth-bars{
|
||||
height:54px;
|
||||
}
|
||||
.pf-growth-card__top{
|
||||
display:flex;
|
||||
align-items:flex-start;
|
||||
justify-content:space-between;
|
||||
gap:10px;
|
||||
align-items:center;
|
||||
min-width:0;
|
||||
}
|
||||
.pf-growth-card__id{
|
||||
.pf-learner-table__head{
|
||||
position:sticky;
|
||||
top:0;
|
||||
z-index:1;
|
||||
padding:9px 12px;
|
||||
border-bottom:1px solid var(--hair);
|
||||
color:var(--text-muted);
|
||||
background:var(--bg-surface);
|
||||
font-size:var(--fs-xs);
|
||||
font-weight:800;
|
||||
white-space:nowrap;
|
||||
}
|
||||
.pf-learner-row{
|
||||
padding:11px 12px;
|
||||
border-top:1px solid var(--paper-2);
|
||||
}
|
||||
.pf-learner-table__head + .pf-learner-row{
|
||||
border-top:0;
|
||||
}
|
||||
.pf-learner-row:hover{
|
||||
background:color-mix(in srgb,var(--accent-tint) 14%,transparent);
|
||||
}
|
||||
.pf-learner-row__id,
|
||||
.pf-learner-row__metric,
|
||||
.pf-learner-row__spark,
|
||||
.pf-learner-row__tags{
|
||||
min-width:0;
|
||||
}
|
||||
.pf-learner-row__id{
|
||||
display:grid;
|
||||
gap:3px;
|
||||
}
|
||||
.pf-growth-card__id b{
|
||||
.pf-learner-row__id b{
|
||||
color:var(--text-strong);
|
||||
font-size:var(--fs-sm);
|
||||
line-height:1.35;
|
||||
}
|
||||
.pf-learner-row__id span,
|
||||
.pf-learner-row__metric span{
|
||||
color:var(--text-muted);
|
||||
font-size:11px;
|
||||
line-height:1.3;
|
||||
overflow:hidden;
|
||||
text-overflow:ellipsis;
|
||||
white-space:nowrap;
|
||||
}
|
||||
.pf-growth-card__id span,
|
||||
.pf-growth-card__metrics small,
|
||||
.pf-growth-point span,
|
||||
.pf-learner-row__metric{
|
||||
display:grid;
|
||||
gap:3px;
|
||||
}
|
||||
.pf-learner-row__metric::before,
|
||||
.pf-learner-row__spark::before,
|
||||
.pf-learner-row__tags::before{
|
||||
display:none;
|
||||
content:attr(data-label);
|
||||
color:var(--text-muted);
|
||||
font-size:var(--fs-xs);
|
||||
font-weight:800;
|
||||
}
|
||||
.pf-learner-row__metric b{
|
||||
color:var(--text-strong);
|
||||
font-family:var(--font-num);
|
||||
font-size:15px;
|
||||
line-height:1.15;
|
||||
white-space:nowrap;
|
||||
}
|
||||
.pf-learner-row__spark{
|
||||
display:grid;
|
||||
grid-template-columns:minmax(0,1fr) auto;
|
||||
gap:8px;
|
||||
align-items:center;
|
||||
}
|
||||
.pf-learner-spark{
|
||||
min-width:0;
|
||||
height:46px;
|
||||
display:flex;
|
||||
align-items:flex-end;
|
||||
gap:4px;
|
||||
padding:6px 6px 4px;
|
||||
border:1px solid var(--paper-2);
|
||||
border-radius:var(--radius-sm);
|
||||
background:var(--bg-surface-2);
|
||||
}
|
||||
.pf-learner-spark .pf-growth-bar{
|
||||
min-width:10px;
|
||||
}
|
||||
.pf-learner-spark .pf-growth-bar small{
|
||||
display:none;
|
||||
}
|
||||
.pf-learner-row__tags{
|
||||
display:flex;
|
||||
flex-wrap:wrap;
|
||||
gap:5px;
|
||||
}
|
||||
.pf-learner-row__tags span{
|
||||
max-width:100%;
|
||||
padding:4px 7px;
|
||||
border:1px solid var(--paper-2);
|
||||
border-radius:999px;
|
||||
color:var(--text-body);
|
||||
background:var(--bg-surface-2);
|
||||
font-size:11px;
|
||||
line-height:1.2;
|
||||
overflow:hidden;
|
||||
text-overflow:ellipsis;
|
||||
white-space:nowrap;
|
||||
}
|
||||
.pf-learner-row__action{
|
||||
display:flex;
|
||||
justify-content:flex-end;
|
||||
}
|
||||
.pf-learner-row__action .vg-btn{
|
||||
min-width:92px;
|
||||
padding-inline:10px;
|
||||
}
|
||||
.pf-recent__review-state{
|
||||
color:var(--text-muted);
|
||||
font-size:12px;
|
||||
|
|
@ -415,37 +547,6 @@
|
|||
margin-top:3px;
|
||||
line-height:1.2;
|
||||
}
|
||||
.pf-growth-card__metrics{
|
||||
display:grid;
|
||||
grid-template-columns:repeat(3,minmax(0,1fr));
|
||||
gap:8px;
|
||||
}
|
||||
.pf-growth-card__metrics span{
|
||||
min-width:0;
|
||||
display:grid;
|
||||
gap:3px;
|
||||
padding:9px 10px;
|
||||
border:1px solid var(--paper-2);
|
||||
border-radius:var(--radius-sm);
|
||||
background:var(--bg-surface-2);
|
||||
}
|
||||
.pf-growth-card__metrics b{
|
||||
color:var(--text-strong);
|
||||
font-family:var(--font-num);
|
||||
font-size:15px;
|
||||
line-height:1.15;
|
||||
white-space:nowrap;
|
||||
}
|
||||
.pf-growth-bars{
|
||||
height:82px;
|
||||
display:flex;
|
||||
align-items:flex-end;
|
||||
gap:6px;
|
||||
padding:8px 8px 6px;
|
||||
border:1px solid var(--paper-2);
|
||||
border-radius:var(--radius-sm);
|
||||
background:color-mix(in srgb,var(--bg-surface-2) 82%,transparent);
|
||||
}
|
||||
.pf-growth-bar{
|
||||
flex:1 1 0;
|
||||
min-width:14px;
|
||||
|
|
@ -472,50 +573,6 @@
|
|||
text-align:center;
|
||||
line-height:1;
|
||||
}
|
||||
.pf-growth-card__tags{
|
||||
min-height:26px;
|
||||
display:flex;
|
||||
flex-wrap:wrap;
|
||||
gap:6px;
|
||||
align-content:flex-start;
|
||||
}
|
||||
.pf-growth-card__tags span{
|
||||
max-width:100%;
|
||||
padding:4px 7px;
|
||||
border:1px solid var(--paper-2);
|
||||
border-radius:999px;
|
||||
color:var(--text-body);
|
||||
background:var(--bg-surface-2);
|
||||
font-size:11px;
|
||||
line-height:1.2;
|
||||
overflow:hidden;
|
||||
text-overflow:ellipsis;
|
||||
white-space:nowrap;
|
||||
}
|
||||
.pf-growth-card__points{
|
||||
display:grid;
|
||||
gap:7px;
|
||||
}
|
||||
.pf-growth-point{
|
||||
min-width:0;
|
||||
display:grid;
|
||||
grid-template-columns:minmax(86px,.5fr) minmax(0,1fr);
|
||||
gap:8px;
|
||||
align-items:center;
|
||||
}
|
||||
.pf-growth-point b{
|
||||
color:var(--text-strong);
|
||||
font-size:12px;
|
||||
line-height:1.35;
|
||||
overflow:hidden;
|
||||
text-overflow:ellipsis;
|
||||
white-space:nowrap;
|
||||
}
|
||||
.pf-growth-point span{
|
||||
overflow:hidden;
|
||||
text-overflow:ellipsis;
|
||||
white-space:nowrap;
|
||||
}
|
||||
.pf-analysis-panel{
|
||||
background:linear-gradient(180deg,var(--bg-surface),color-mix(in srgb,var(--bg-surface-2) 72%,var(--bg-surface)));
|
||||
}
|
||||
|
|
@ -525,6 +582,54 @@
|
|||
gap:12px;
|
||||
padding:12px;
|
||||
}
|
||||
.pf-analysis-tabs{
|
||||
min-width:0;
|
||||
display:grid;
|
||||
grid-template-columns:repeat(3,minmax(0,1fr));
|
||||
border:1px solid var(--paper-2);
|
||||
border-radius:var(--radius-sm);
|
||||
overflow:hidden;
|
||||
background:var(--bg-surface-2);
|
||||
}
|
||||
.pf-analysis-tabs button{
|
||||
min-width:0;
|
||||
min-height:48px;
|
||||
display:grid;
|
||||
gap:3px;
|
||||
align-content:center;
|
||||
padding:8px 12px;
|
||||
border:0;
|
||||
border-left:1px solid var(--hair);
|
||||
color:var(--text-muted);
|
||||
background:transparent;
|
||||
font:inherit;
|
||||
text-align:left;
|
||||
cursor:pointer;
|
||||
}
|
||||
.pf-analysis-tabs button:first-child{
|
||||
border-left:0;
|
||||
}
|
||||
.pf-analysis-tabs button:hover,
|
||||
.pf-analysis-tabs button:focus-visible{
|
||||
background:var(--bg-surface);
|
||||
}
|
||||
.pf-analysis-tabs button:focus-visible{
|
||||
outline:2px solid var(--accent);
|
||||
outline-offset:-2px;
|
||||
}
|
||||
.pf-analysis-tabs button.is-active{
|
||||
color:var(--text-strong);
|
||||
background:color-mix(in srgb,var(--accent-tint) 32%,var(--bg-surface));
|
||||
}
|
||||
.pf-analysis-tabs b{
|
||||
font-size:var(--fs-sm);
|
||||
line-height:1.2;
|
||||
}
|
||||
.pf-analysis-tabs span{
|
||||
color:var(--text-muted);
|
||||
font-size:11px;
|
||||
line-height:1.2;
|
||||
}
|
||||
.pf-analysis__top{
|
||||
min-width:0;
|
||||
display:grid;
|
||||
|
|
@ -594,12 +699,6 @@
|
|||
overflow:hidden;
|
||||
text-overflow:ellipsis;
|
||||
}
|
||||
.pf-analysis__body{
|
||||
min-width:0;
|
||||
display:grid;
|
||||
grid-template-columns:minmax(0,1.25fr) minmax(240px,.75fr);
|
||||
gap:12px;
|
||||
}
|
||||
.pf-analysis__chart,
|
||||
.pf-analysis__stages,
|
||||
.pf-analysis__timeline{
|
||||
|
|
@ -1125,13 +1224,9 @@
|
|||
}
|
||||
@media (max-width:1100px){
|
||||
.pf-signal-strip,
|
||||
.pf-workspace,
|
||||
.pf-analysis-shell{
|
||||
.pf-workspace{
|
||||
grid-template-columns:1fr;
|
||||
}
|
||||
.pf-analysis-rail{
|
||||
position:static;
|
||||
}
|
||||
.pf-triage{
|
||||
grid-template-columns:auto minmax(0,max-content) auto;
|
||||
justify-content:start;
|
||||
|
|
@ -1141,12 +1236,57 @@
|
|||
}
|
||||
.pf-kpi:nth-child(n+2){border-left:1px solid var(--hair);}
|
||||
.pf-kpi:nth-child(n+4){border-top:0;}
|
||||
.pf-growth-list{
|
||||
grid-template-columns:repeat(2,minmax(0,1fr));
|
||||
.pf-learner-toolbar{
|
||||
grid-template-columns:1fr;
|
||||
}
|
||||
.pf-analysis-rail .pf-growth-list{
|
||||
max-height:360px;
|
||||
grid-template-columns:repeat(2,minmax(0,1fr));
|
||||
.pf-sort-segment{
|
||||
justify-content:flex-start;
|
||||
overflow:auto;
|
||||
scrollbar-gutter:stable;
|
||||
}
|
||||
.pf-learner-table{
|
||||
background:var(--bg-surface-2);
|
||||
}
|
||||
.pf-learner-table__head{
|
||||
display:none;
|
||||
}
|
||||
.pf-learner-row{
|
||||
margin:10px;
|
||||
padding:12px;
|
||||
border:1px solid var(--paper-2);
|
||||
border-radius:var(--radius);
|
||||
background:var(--bg-surface);
|
||||
grid-template-columns:repeat(3,minmax(0,1fr));
|
||||
gap:10px 12px;
|
||||
}
|
||||
.pf-learner-row + .pf-learner-row{
|
||||
margin-top:0;
|
||||
}
|
||||
.pf-learner-row__id,
|
||||
.pf-learner-row__spark,
|
||||
.pf-learner-row__tags,
|
||||
.pf-learner-row__action{
|
||||
grid-column:1 / -1;
|
||||
}
|
||||
.pf-learner-row__metric::before,
|
||||
.pf-learner-row__spark::before,
|
||||
.pf-learner-row__tags::before{
|
||||
display:block;
|
||||
}
|
||||
.pf-learner-row__spark,
|
||||
.pf-learner-row__tags{
|
||||
display:grid;
|
||||
grid-template-columns:minmax(76px,.28fr) minmax(0,1fr);
|
||||
align-items:center;
|
||||
}
|
||||
.pf-learner-row__tags{
|
||||
display:flex;
|
||||
}
|
||||
.pf-learner-row__tags::before{
|
||||
flex:0 0 76px;
|
||||
}
|
||||
.pf-learner-row__action{
|
||||
justify-content:flex-start;
|
||||
}
|
||||
.pf-analysis__top{
|
||||
grid-template-columns:1fr;
|
||||
|
|
@ -1160,9 +1300,6 @@
|
|||
.pf-analysis__metrics span:nth-child(n+4){
|
||||
border-top:1px solid var(--hair);
|
||||
}
|
||||
.pf-analysis__body{
|
||||
grid-template-columns:1fr;
|
||||
}
|
||||
.pf-timeline-row{
|
||||
grid-template-columns:34px minmax(0,1fr) minmax(190px,.85fr) auto;
|
||||
}
|
||||
|
|
@ -1184,6 +1321,9 @@
|
|||
width:100%;
|
||||
justify-content:flex-start;
|
||||
}
|
||||
.pf-analysis-detail-head{
|
||||
grid-template-columns:1fr;
|
||||
}
|
||||
.pf-depth-strip{
|
||||
grid-template-columns:repeat(2,minmax(0,1fr));
|
||||
}
|
||||
|
|
@ -1204,15 +1344,19 @@
|
|||
.pf-kpi:nth-child(n+2){border-left:0;}
|
||||
.pf-kpi:nth-child(even){border-left:1px solid var(--hair);}
|
||||
.pf-kpi:nth-child(n+3){border-top:1px solid var(--hair);}
|
||||
.pf-growth-list{
|
||||
grid-template-columns:1fr;
|
||||
}
|
||||
.pf-analysis-rail .pf-growth-list{
|
||||
grid-template-columns:1fr;
|
||||
}
|
||||
.pf-analysis{
|
||||
padding:10px;
|
||||
}
|
||||
.pf-analysis-tabs{
|
||||
grid-template-columns:1fr;
|
||||
}
|
||||
.pf-analysis-tabs button,
|
||||
.pf-analysis-tabs button:first-child{
|
||||
border-left:0;
|
||||
}
|
||||
.pf-analysis-tabs button:nth-child(n+2){
|
||||
border-top:1px solid var(--hair);
|
||||
}
|
||||
.pf-analysis__metrics{
|
||||
grid-template-columns:repeat(2,minmax(0,1fr));
|
||||
}
|
||||
|
|
@ -1345,11 +1489,38 @@
|
|||
.pf-depth-strip span:nth-child(n+2){
|
||||
border-top:1px solid var(--hair);
|
||||
}
|
||||
.pf-growth-list{
|
||||
.pf-learner-toolbar{
|
||||
padding:10px;
|
||||
}
|
||||
.pf-growth-card__metrics{
|
||||
.pf-sort-segment{
|
||||
display:grid;
|
||||
grid-template-columns:repeat(2,minmax(0,1fr));
|
||||
overflow:hidden;
|
||||
}
|
||||
.pf-sort-segment button,
|
||||
.pf-sort-segment button:first-child{
|
||||
border-left:0;
|
||||
}
|
||||
.pf-sort-segment button:nth-child(even){
|
||||
border-left:1px solid var(--hair);
|
||||
}
|
||||
.pf-sort-segment button:nth-child(n+3){
|
||||
border-top:1px solid var(--hair);
|
||||
}
|
||||
.pf-learner-row{
|
||||
grid-template-columns:repeat(2,minmax(0,1fr));
|
||||
margin:8px;
|
||||
}
|
||||
.pf-learner-row__spark,
|
||||
.pf-learner-row__tags{
|
||||
grid-template-columns:1fr;
|
||||
gap:6px;
|
||||
}
|
||||
.pf-learner-row__tags::before{
|
||||
flex-basis:100%;
|
||||
}
|
||||
.pf-learner-row__action .vg-btn{
|
||||
width:100%;
|
||||
}
|
||||
.pf-analysis__metrics{
|
||||
grid-template-columns:1fr;
|
||||
|
|
@ -1379,14 +1550,6 @@
|
|||
.pf-timeline-row__dates{
|
||||
grid-template-columns:1fr;
|
||||
}
|
||||
.pf-growth-point{
|
||||
grid-template-columns:1fr;
|
||||
gap:2px;
|
||||
}
|
||||
.pf-growth-point b,
|
||||
.pf-growth-point span{
|
||||
white-space:normal;
|
||||
}
|
||||
.pf-alert{
|
||||
grid-template-columns:auto minmax(0,1fr);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue