평가 및 화면 구조 정리
This commit is contained in:
parent
1248ae8ca4
commit
391639c1de
44 changed files with 5816 additions and 4501 deletions
140
apps/api/app/services/case_worksheet_rubric.py
Normal file
140
apps/api/app/services/case_worksheet_rubric.py
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
"""Validation helpers for externally owned case worksheet rubrics.
|
||||
|
||||
The clinical team owns scoring criteria. This module only validates the
|
||||
machine-readable scaffold that lets those criteria live outside application
|
||||
code.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any, Mapping
|
||||
|
||||
SCHEMA_VERSION = "vignette.case_worksheet_rubric.v1"
|
||||
VALID_STATUSES = {"scaffold_only", "draft", "approved"}
|
||||
EXPECTED_CONTENT_OWNER = "clinical_team"
|
||||
|
||||
|
||||
def load_rubric(path: Path) -> dict[str, Any]:
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
if not isinstance(data, dict):
|
||||
raise ValueError("case worksheet rubric must be a JSON object")
|
||||
return data
|
||||
|
||||
|
||||
def validate_rubric(
|
||||
rubric: Mapping[str, Any],
|
||||
*,
|
||||
expected_item_keys: Mapping[str, set[str]] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
errors: list[str] = []
|
||||
warnings: list[str] = []
|
||||
|
||||
schema_version = str(rubric.get("schema_version") or "")
|
||||
status = str(rubric.get("status") or "")
|
||||
scoring_enabled = bool(rubric.get("scoring_enabled"))
|
||||
sections = rubric.get("sections")
|
||||
|
||||
if schema_version != SCHEMA_VERSION:
|
||||
errors.append("schema_version must be vignette.case_worksheet_rubric.v1")
|
||||
if status not in VALID_STATUSES:
|
||||
errors.append("status must be one of scaffold_only, draft, approved")
|
||||
if str(rubric.get("content_owner") or "") != EXPECTED_CONTENT_OWNER:
|
||||
errors.append("content_owner must be clinical_team")
|
||||
if scoring_enabled and status != "approved":
|
||||
errors.append("scoring_enabled requires status=approved")
|
||||
if status == "approved":
|
||||
approval = rubric.get("approval")
|
||||
if not isinstance(approval, Mapping):
|
||||
errors.append("approved rubric requires approval metadata")
|
||||
else:
|
||||
if not str(approval.get("clinical_reviewer") or ""):
|
||||
errors.append("approved rubric requires approval.clinical_reviewer")
|
||||
if not str(approval.get("approved_at") or ""):
|
||||
errors.append("approved rubric requires approval.approved_at")
|
||||
|
||||
section_count = 0
|
||||
item_count = 0
|
||||
section_item_keys: dict[str, set[str]] = {}
|
||||
if not isinstance(sections, list) or not sections:
|
||||
errors.append("sections must be a non-empty list")
|
||||
else:
|
||||
seen_sections: set[str] = set()
|
||||
for section in sections:
|
||||
if not isinstance(section, Mapping):
|
||||
errors.append("each section must be an object")
|
||||
continue
|
||||
section_key = str(section.get("key") or "")
|
||||
if not section_key:
|
||||
errors.append("section.key is required")
|
||||
continue
|
||||
if section_key in seen_sections:
|
||||
errors.append(f"duplicate section key: {section_key}")
|
||||
seen_sections.add(section_key)
|
||||
section_count += 1
|
||||
items = section.get("items")
|
||||
if not isinstance(items, list) or not items:
|
||||
errors.append(f"{section_key}: items must be a non-empty list")
|
||||
continue
|
||||
seen_items: set[str] = set()
|
||||
for item in items:
|
||||
if not isinstance(item, Mapping):
|
||||
errors.append(f"{section_key}: each item must be an object")
|
||||
continue
|
||||
item_key = str(item.get("key") or "")
|
||||
if not item_key:
|
||||
errors.append(f"{section_key}: item.key is required")
|
||||
continue
|
||||
if item_key in seen_items:
|
||||
errors.append(f"{section_key}: duplicate item key: {item_key}")
|
||||
seen_items.add(item_key)
|
||||
item_count += 1
|
||||
criteria = item.get("criteria")
|
||||
score_scale = item.get("score_scale")
|
||||
if scoring_enabled:
|
||||
if not isinstance(criteria, list) or not criteria:
|
||||
errors.append(f"{section_key}.{item_key}: scoring requires non-empty criteria")
|
||||
if not _valid_score_scale(score_scale):
|
||||
errors.append(f"{section_key}.{item_key}: scoring requires a valid score_scale")
|
||||
elif not criteria:
|
||||
warnings.append(f"{section_key}.{item_key}: criteria pending clinical team input")
|
||||
section_item_keys[section_key] = seen_items
|
||||
|
||||
if expected_item_keys is not None:
|
||||
expected_sections = set(expected_item_keys)
|
||||
actual_sections = set(section_item_keys)
|
||||
for missing_section in sorted(expected_sections - actual_sections):
|
||||
errors.append(f"missing worksheet section: {missing_section}")
|
||||
for extra_section in sorted(actual_sections - expected_sections):
|
||||
errors.append(f"unexpected worksheet section: {extra_section}")
|
||||
for section_key in sorted(expected_sections & actual_sections):
|
||||
missing_items = expected_item_keys[section_key] - section_item_keys[section_key]
|
||||
extra_items = section_item_keys[section_key] - expected_item_keys[section_key]
|
||||
for item_key in sorted(missing_items):
|
||||
errors.append(f"{section_key}: missing worksheet item: {item_key}")
|
||||
for item_key in sorted(extra_items):
|
||||
errors.append(f"{section_key}: unexpected worksheet item: {item_key}")
|
||||
|
||||
return {
|
||||
"schema_version": SCHEMA_VERSION,
|
||||
"rubric_id": str(rubric.get("rubric_id") or ""),
|
||||
"status": status,
|
||||
"scoring_enabled": scoring_enabled,
|
||||
"sections_total": section_count,
|
||||
"items_total": item_count,
|
||||
"passed": not errors,
|
||||
"errors": errors,
|
||||
"warnings": warnings,
|
||||
}
|
||||
|
||||
|
||||
def _valid_score_scale(value: object) -> bool:
|
||||
if not isinstance(value, Mapping):
|
||||
return False
|
||||
minimum = value.get("min")
|
||||
maximum = value.get("max")
|
||||
anchors = value.get("anchors")
|
||||
if not isinstance(minimum, int) or not isinstance(maximum, int) or minimum >= maximum:
|
||||
return False
|
||||
return isinstance(anchors, list) and len(anchors) >= 2
|
||||
|
|
@ -42,13 +42,21 @@ _KOREAN_SURNAME_CHARS = (
|
|||
"명기반왕금옥육인맹제모탁국어은편용예봉경"
|
||||
)
|
||||
_KOREAN_FULL_NAME = rf"[{_KOREAN_SURNAME_CHARS}][가-힣]{{1,3}}"
|
||||
_KOREAN_FULL_NAME_BEFORE_SUFFIX = rf"[{_KOREAN_SURNAME_CHARS}][가-힣]{{1,3}}?"
|
||||
_KOREAN_NAME_STOPWORDS = {
|
||||
"연락",
|
||||
"연락처",
|
||||
"이메일",
|
||||
"주민번호",
|
||||
"번호",
|
||||
"이름",
|
||||
"이야기",
|
||||
"생각",
|
||||
"마음",
|
||||
"기분",
|
||||
"상담",
|
||||
"기록",
|
||||
"진료",
|
||||
"학교",
|
||||
"엄마",
|
||||
"아빠",
|
||||
|
|
@ -82,12 +90,33 @@ _PII_PATTERNS: list[tuple[str, re.Pattern[str]]] = [
|
|||
r"(?=$|[\s,.;!?。])"
|
||||
),
|
||||
),
|
||||
# 한국어 이름: "제 이름은 김서연입니다", "보호자 이름은 박민수입니다" 같은 자연 발화형 라벨.
|
||||
(
|
||||
"NAME",
|
||||
re.compile(
|
||||
r"(?P<prefix>(?:(?:제|저의|내|나의|보호자|학생|내담자|상담자|친구|엄마|아빠|어머니|아버지)\s+)?"
|
||||
r"(?:이름|성명|실명|본명)\s*(?:은|는|이|가)?\s*)"
|
||||
rf"(?P<value>{_KOREAN_FULL_NAME_BEFORE_SUFFIX})"
|
||||
r"(?P<suffix>\s*(?:입니다|이에요|예요|이고|이고요|이라고|라고)?)"
|
||||
r"(?=$|[\s,.;!?。])"
|
||||
),
|
||||
),
|
||||
# 한국어 이름: "저는 김서연입니다", "제가 박민수예요", "김서연입니다" 같은 자기소개형 문장.
|
||||
(
|
||||
"NAME",
|
||||
re.compile(
|
||||
r"(?P<prefix>(?:(?:저는|나는|제가|내가)\s*)?)"
|
||||
rf"(?P<value>{_KOREAN_FULL_NAME_BEFORE_SUFFIX})"
|
||||
r"(?P<suffix>\s*(?:입니다|이에요|예요|이고|이고요))"
|
||||
r"(?=$|[\s,.;!?。])"
|
||||
),
|
||||
),
|
||||
# 한국어 이름: 역할/관계 명사 뒤에 붙은 인명 + 조사/호칭.
|
||||
(
|
||||
"NAME",
|
||||
re.compile(
|
||||
r"(?P<prefix>(?:내담자|상담자|학생|보호자|담임|교수|선생님|친구|엄마|아빠|어머니|아버지|동생|언니|오빠|형|누나)\s+)"
|
||||
rf"(?P<value>{_KOREAN_FULL_NAME})"
|
||||
rf"(?P<value>{_KOREAN_FULL_NAME_BEFORE_SUFFIX})"
|
||||
r"(?P<suffix>\s*(?:님|씨|학생|상담자|내담자)?"
|
||||
r"(?:은|는|이|가|을|를|와|과|에게|한테|라고|이라는|입니다|이에요|예요|이고|이고요))"
|
||||
),
|
||||
|
|
@ -96,7 +125,7 @@ _PII_PATTERNS: list[tuple[str, re.Pattern[str]]] = [
|
|||
(
|
||||
"NAME",
|
||||
re.compile(
|
||||
rf"(?<![가-힣])(?P<value>{_KOREAN_FULL_NAME})"
|
||||
rf"(?<![가-힣])(?P<value>{_KOREAN_FULL_NAME_BEFORE_SUFFIX})"
|
||||
r"(?P<suffix>(?:은|는|이|가|을|를|와|과|에게|한테|라고|이라는))"
|
||||
),
|
||||
),
|
||||
|
|
@ -104,21 +133,21 @@ _PII_PATTERNS: list[tuple[str, re.Pattern[str]]] = [
|
|||
(
|
||||
"NAME",
|
||||
re.compile(
|
||||
rf"(?<![가-힣])(?P<value>{_KOREAN_FULL_NAME})"
|
||||
rf"(?<![가-힣])(?P<value>{_KOREAN_FULL_NAME_BEFORE_SUFFIX})"
|
||||
r"(?P<suffix>\s?(?:씨|님)(?:은|는|이|가|을|를|와|과|에게|한테|고|이고|인데)?)"
|
||||
r"(?=$|[\s,.;!?。])"
|
||||
),
|
||||
),
|
||||
# 주민등록번호 (6자리-7자리)
|
||||
("RRN", re.compile(r"\b\d{6}[-\s]?\d{7}\b")),
|
||||
("RRN", re.compile(r"(?<!\d)\d{6}[-\s]?\d{7}(?!\d)")),
|
||||
# 휴대폰 (010-1234-5678 등)
|
||||
("PHONE", re.compile(r"\b01[016789][-\s]?\d{3,4}[-\s]?\d{4}\b")),
|
||||
("PHONE", re.compile(r"(?<!\d)01[016789][-\s]?\d{3,4}[-\s]?\d{4}(?!\d)")),
|
||||
# 일반 전화
|
||||
("PHONE", re.compile(r"\b0\d{1,2}[-\s]?\d{3,4}[-\s]?\d{4}\b")),
|
||||
("PHONE", re.compile(r"(?<!\d)0\d{1,2}[-\s]?\d{3,4}[-\s]?\d{4}(?!\d)")),
|
||||
# 이메일
|
||||
("EMAIL", re.compile(r"\b[\w.+-]+@[\w-]+\.[\w.-]+\b")),
|
||||
# 카드/계좌 유사 긴 숫자열 (12자리 이상)
|
||||
("NUMID", re.compile(r"\b\d{12,}\b")),
|
||||
("NUMID", re.compile(r"(?<!\d)\d{12,}(?!\d)")),
|
||||
# 구체적 날짜(생년월일 등): 2001.4.18 / 2001-04-18 / 2001년 4월 18일
|
||||
("DATE", re.compile(r"(?:19|20)\d{2}\s?[.\-/년]\s?\d{1,2}\s?[.\-/월]\s?\d{1,2}\s?일?")),
|
||||
# 금액(원): 1,200원 / 1200원 (3자리+ 또는 콤마구분) — 식별 맥락 보호
|
||||
|
|
|
|||
|
|
@ -10,6 +10,16 @@ from . import guardrail
|
|||
|
||||
MaskFunc = Callable[[str], guardrail.MaskResult]
|
||||
|
||||
REPORT_SCHEMA_VERSION = "vignette.pii_masking_eval_report.v1"
|
||||
INPUT_SCHEMA_VERSION = "vignette.pii_masking_eval_input.v1"
|
||||
|
||||
DEFAULT_CASE_META = {
|
||||
"locale": "ko-KR",
|
||||
"source": "synthetic",
|
||||
"category": "unspecified",
|
||||
"severity": "medium",
|
||||
}
|
||||
|
||||
|
||||
def load_cases(path: Path) -> list[dict[str, Any]]:
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
|
|
@ -18,9 +28,18 @@ def load_cases(path: Path) -> list[dict[str, Any]]:
|
|||
return [dict(item) for item in data]
|
||||
|
||||
|
||||
def evaluate_case(case: Mapping[str, Any], *, mask_func: MaskFunc = guardrail.mask_pii) -> dict[str, Any]:
|
||||
def evaluate_case(
|
||||
case: Mapping[str, Any],
|
||||
*,
|
||||
mask_func: MaskFunc = guardrail.mask_pii,
|
||||
include_evidence_text: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
case_id = str(case.get("id") or "")
|
||||
text = str(case.get("text") or "")
|
||||
locale = str(case.get("locale") or DEFAULT_CASE_META["locale"])
|
||||
source = str(case.get("source") or DEFAULT_CASE_META["source"])
|
||||
category = str(case.get("category") or DEFAULT_CASE_META["category"])
|
||||
severity = str(case.get("severity") or DEFAULT_CASE_META["severity"])
|
||||
result = mask_func(text)
|
||||
entities = set(result.entities)
|
||||
expected_entities = {str(item) for item in case.get("expected_entities") or []}
|
||||
|
|
@ -34,25 +53,36 @@ def evaluate_case(case: Mapping[str, Any], *, mask_func: MaskFunc = guardrail.ma
|
|||
required_missing = [item for item in required_substrings if item and item not in result.text_masked]
|
||||
passed = not (missing_entities or unexpected_detected or forbidden_remaining or required_missing)
|
||||
|
||||
return {
|
||||
report = {
|
||||
"id": case_id,
|
||||
"locale": locale,
|
||||
"source": source,
|
||||
"category": category,
|
||||
"severity": severity,
|
||||
"passed": passed,
|
||||
"entities": sorted(entities),
|
||||
"masked_text": result.text_masked,
|
||||
"missing_entities": missing_entities,
|
||||
"unexpected_entities": unexpected_detected,
|
||||
"forbidden_remaining": forbidden_remaining,
|
||||
"forbidden_remaining_count": len(forbidden_remaining),
|
||||
"required_missing": required_missing,
|
||||
}
|
||||
if include_evidence_text:
|
||||
report["masked_text"] = result.text_masked
|
||||
report["forbidden_remaining"] = forbidden_remaining
|
||||
return report
|
||||
|
||||
|
||||
def evaluate_cases(
|
||||
cases: Iterable[Mapping[str, Any]],
|
||||
*,
|
||||
mask_func: MaskFunc = guardrail.mask_pii,
|
||||
include_evidence_text: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
case_list = list(cases)
|
||||
results = [evaluate_case(case, mask_func=mask_func) for case in case_list]
|
||||
results = [
|
||||
evaluate_case(case, mask_func=mask_func, include_evidence_text=include_evidence_text)
|
||||
for case in case_list
|
||||
]
|
||||
total_expected_entities = 0
|
||||
matched_expected_entities = 0
|
||||
total_forbidden = 0
|
||||
|
|
@ -64,11 +94,17 @@ def evaluate_cases(
|
|||
total_expected_entities += len(expected_entities)
|
||||
matched_expected_entities += len(expected_entities) - len(result["missing_entities"])
|
||||
total_forbidden += len(forbidden)
|
||||
removed_forbidden += len(forbidden) - len(result["forbidden_remaining"])
|
||||
remaining_forbidden = int(result.get("forbidden_remaining_count", len(result.get("forbidden_remaining", []))))
|
||||
removed_forbidden += len(forbidden) - remaining_forbidden
|
||||
unexpected_violations += len(result["unexpected_entities"])
|
||||
|
||||
passed_cases = sum(1 for result in results if result["passed"])
|
||||
return {
|
||||
"schema_version": REPORT_SCHEMA_VERSION,
|
||||
"input_schema_version": INPUT_SCHEMA_VERSION,
|
||||
"run_mode": "technical_dry_run",
|
||||
"data_source": "local_fixture",
|
||||
"evidence_text_included": include_evidence_text,
|
||||
"passed": passed_cases == len(results),
|
||||
"cases_total": len(results),
|
||||
"cases_passed": passed_cases,
|
||||
|
|
@ -76,15 +112,45 @@ def evaluate_cases(
|
|||
"expected_entity_recall": _ratio(matched_expected_entities, total_expected_entities),
|
||||
"forbidden_substring_removal": _ratio(removed_forbidden, total_forbidden),
|
||||
"unexpected_entity_violations": unexpected_violations,
|
||||
"by_source": _breakdown(case_list, results, "source"),
|
||||
"by_category": _breakdown(case_list, results, "category"),
|
||||
"by_severity": _breakdown(case_list, results, "severity"),
|
||||
"results": results,
|
||||
}
|
||||
|
||||
|
||||
def evaluate_fixture(path: Path, *, mask_func: MaskFunc = guardrail.mask_pii) -> dict[str, Any]:
|
||||
return evaluate_cases(load_cases(path), mask_func=mask_func)
|
||||
def evaluate_fixture(
|
||||
path: Path,
|
||||
*,
|
||||
mask_func: MaskFunc = guardrail.mask_pii,
|
||||
include_evidence_text: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
return evaluate_cases(load_cases(path), mask_func=mask_func, include_evidence_text=include_evidence_text)
|
||||
|
||||
|
||||
def _ratio(numerator: int, denominator: int) -> float:
|
||||
if denominator <= 0:
|
||||
return 1.0
|
||||
return round(numerator / denominator, 4)
|
||||
|
||||
|
||||
def _case_meta(case: Mapping[str, Any], key: str) -> str:
|
||||
fallback = DEFAULT_CASE_META.get(key, "unspecified")
|
||||
return str(case.get(key) or fallback)
|
||||
|
||||
|
||||
def _breakdown(
|
||||
cases: list[Mapping[str, Any]],
|
||||
results: list[Mapping[str, Any]],
|
||||
key: str,
|
||||
) -> dict[str, dict[str, int]]:
|
||||
grouped: dict[str, dict[str, int]] = {}
|
||||
for case, result in zip(cases, results):
|
||||
value = _case_meta(case, key)
|
||||
bucket = grouped.setdefault(value, {"cases_total": 0, "cases_passed": 0, "cases_failed": 0})
|
||||
bucket["cases_total"] += 1
|
||||
if result.get("passed"):
|
||||
bucket["cases_passed"] += 1
|
||||
else:
|
||||
bucket["cases_failed"] += 1
|
||||
return dict(sorted(grouped.items()))
|
||||
|
|
|
|||
|
|
@ -21,6 +21,9 @@ from .services import session_metrics
|
|||
from .store import InProcSession, TurnRecord
|
||||
|
||||
StageLabel = Literal["라포", "탐색", "개입", "정리"]
|
||||
WorksheetSpeaker = Literal["learner", "client"]
|
||||
WorksheetItemSpec = tuple[str, str, list[str], WorksheetSpeaker | None]
|
||||
WorksheetSectionSpec = tuple[str, str, list[WorksheetItemSpec]]
|
||||
|
||||
LEARNER_VISIBLE_AI_ROLE = "counselor"
|
||||
_PHASE_KEY_BY_LABEL = {
|
||||
|
|
@ -259,6 +262,68 @@ class ReviewCaseWorksheetSaveRequest(BaseModel):
|
|||
limitations: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
CASE_WORKSHEET_SECTION_SPECS: list[WorksheetSectionSpec] = [
|
||||
(
|
||||
"exploration_11",
|
||||
"탐색 11항목",
|
||||
[
|
||||
("presenting_complaint", "주호소", ["힘들", "문제", "걱정", "불안", "우울", "스트레스", "관계"], "client"),
|
||||
("trigger_context", "계기·상황", ["언제", "상황", "최근", "계기", "때"], "client"),
|
||||
("emotion", "정서", ["불안", "우울", "화", "슬프", "답답", "무섭", "외롭", "걱정"], "client"),
|
||||
("cognition", "생각", ["생각", "느낌", "해야", "못", "실패", "의미"], "client"),
|
||||
("behavior", "행동", ["피하", "잠", "먹", "울", "말", "연락", "공부", "멈"], "client"),
|
||||
("body", "신체·수면", ["잠", "식욕", "몸", "두통", "심장", "숨", "피곤"], "client"),
|
||||
("relationship", "관계", ["친구", "가족", "부모", "엄마", "아빠", "교수", "사람", "관계"], "client"),
|
||||
("resources", "자원", ["도움", "지지", "친구", "상담", "선생님", "가족"], "client"),
|
||||
("risk", "위험 신호", ["죽", "자살", "해치", "사라지고", "끝내", "위험"], "client"),
|
||||
("motivation", "변화동기", ["원", "바라", "변화", "해보고", "싶"], None),
|
||||
("first_goal", "상담 목표 초안", ["목표", "계획", "다음", "해볼", "원하"], "learner"),
|
||||
],
|
||||
),
|
||||
(
|
||||
"five_domains",
|
||||
"호소 5영역",
|
||||
[
|
||||
("domain_emotion", "정서", ["불안", "우울", "화", "슬프", "답답", "외롭"], "client"),
|
||||
("domain_cognition", "인지", ["생각", "걱정", "실패", "못", "의미"], "client"),
|
||||
("domain_behavior", "행동", ["피하", "연락", "공부", "잠", "멈"], "client"),
|
||||
("domain_relationship", "대인관계", ["친구", "가족", "사람", "관계", "부모"], "client"),
|
||||
("domain_body", "신체", ["잠", "식욕", "몸", "두통", "피곤", "숨"], "client"),
|
||||
],
|
||||
),
|
||||
(
|
||||
"cognitive_triad_emotions",
|
||||
"인지삼제·1/2차 감정",
|
||||
[
|
||||
("triad_self", "자기", ["나는", "내가", "나 자신", "스스로"], "client"),
|
||||
("triad_world", "타인·세계", ["사람", "세상", "학교", "가족", "친구"], "client"),
|
||||
("triad_future", "미래", ["앞으로", "미래", "계속", "나중"], "client"),
|
||||
("primary_emotion", "1차 감정", ["불안", "슬프", "무섭", "외롭", "걱정"], "client"),
|
||||
("secondary_emotion", "2차 감정", ["화", "짜증", "수치", "죄책", "부끄"], "client"),
|
||||
],
|
||||
),
|
||||
(
|
||||
"protective_barrier_quadrants",
|
||||
"보호·방해 4사분면",
|
||||
[
|
||||
("internal_protective", "내적 보호요인", ["해보고", "버텼", "노력", "원", "견뎠"], None),
|
||||
("internal_barrier", "내적 방해요인", ["못", "두려", "불안", "회피", "걱정"], "client"),
|
||||
("external_protective", "외적 보호요인", ["친구", "가족", "상담", "교수", "도움"], "client"),
|
||||
("external_barrier", "외적 방해요인", ["갈등", "압박", "비난", "스트레스", "혼자"], "client"),
|
||||
],
|
||||
),
|
||||
(
|
||||
"biopsychosocial_goals",
|
||||
"생물·심리·사회 목표",
|
||||
[
|
||||
("bio_goal", "생물", ["잠", "식사", "운동", "몸", "피곤"], "client"),
|
||||
("psy_goal", "심리", ["생각", "감정", "불안", "연습", "조절"], None),
|
||||
("social_goal", "사회", ["관계", "대화", "연락", "도움", "친구"], None),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
class SessionTeacherReviewStatus(BaseModel):
|
||||
status: Literal["pending", "viewed", "closed"] = "pending"
|
||||
note: str = ""
|
||||
|
|
@ -824,7 +889,7 @@ def _worksheet_item(
|
|||
def _worksheet_section(
|
||||
key: str,
|
||||
title: str,
|
||||
specs: list[tuple[str, str, list[str], Literal["learner", "client"] | None]],
|
||||
specs: list[WorksheetItemSpec],
|
||||
turns: list[ReviewTurn],
|
||||
fallback_client: ReviewTurn | None,
|
||||
fallback_learner: ReviewTurn | None,
|
||||
|
|
@ -845,6 +910,13 @@ def _worksheet_section(
|
|||
return ReviewWorksheetSection(key=key, title=title, items=items)
|
||||
|
||||
|
||||
def case_worksheet_template_item_keys() -> dict[str, set[str]]:
|
||||
return {
|
||||
section_key: {item_key for item_key, _, _, _ in item_specs}
|
||||
for section_key, _, item_specs in CASE_WORKSHEET_SECTION_SPECS
|
||||
}
|
||||
|
||||
|
||||
def case_worksheet_from_turns(turns: list[ReviewTurn]) -> ReviewCaseWorksheet:
|
||||
if not turns:
|
||||
return ReviewCaseWorksheet(
|
||||
|
|
@ -855,68 +927,6 @@ def case_worksheet_from_turns(turns: list[ReviewTurn]) -> ReviewCaseWorksheet:
|
|||
|
||||
fallback_client = next((turn for turn in turns if turn.speaker == "client"), None)
|
||||
fallback_learner = next((turn for turn in turns if turn.speaker == "learner"), None)
|
||||
section_specs: list[
|
||||
tuple[str, str, list[tuple[str, str, list[str], Literal["learner", "client"] | None]]]
|
||||
] = [
|
||||
(
|
||||
"exploration_11",
|
||||
"탐색 11항목",
|
||||
[
|
||||
("presenting_complaint", "주호소", ["힘들", "문제", "걱정", "불안", "우울", "스트레스", "관계"], "client"),
|
||||
("trigger_context", "계기·상황", ["언제", "상황", "최근", "계기", "때"], "client"),
|
||||
("emotion", "정서", ["불안", "우울", "화", "슬프", "답답", "무섭", "외롭", "걱정"], "client"),
|
||||
("cognition", "생각", ["생각", "느낌", "해야", "못", "실패", "의미"], "client"),
|
||||
("behavior", "행동", ["피하", "잠", "먹", "울", "말", "연락", "공부", "멈"], "client"),
|
||||
("body", "신체·수면", ["잠", "식욕", "몸", "두통", "심장", "숨", "피곤"], "client"),
|
||||
("relationship", "관계", ["친구", "가족", "부모", "엄마", "아빠", "교수", "사람", "관계"], "client"),
|
||||
("resources", "자원", ["도움", "지지", "친구", "상담", "선생님", "가족"], "client"),
|
||||
("risk", "위험 신호", ["죽", "자살", "해치", "사라지고", "끝내", "위험"], "client"),
|
||||
("motivation", "변화동기", ["원", "바라", "변화", "해보고", "싶"], None),
|
||||
("first_goal", "상담 목표 초안", ["목표", "계획", "다음", "해볼", "원하"], "learner"),
|
||||
],
|
||||
),
|
||||
(
|
||||
"five_domains",
|
||||
"호소 5영역",
|
||||
[
|
||||
("domain_emotion", "정서", ["불안", "우울", "화", "슬프", "답답", "외롭"], "client"),
|
||||
("domain_cognition", "인지", ["생각", "걱정", "실패", "못", "의미"], "client"),
|
||||
("domain_behavior", "행동", ["피하", "연락", "공부", "잠", "멈"], "client"),
|
||||
("domain_relationship", "대인관계", ["친구", "가족", "사람", "관계", "부모"], "client"),
|
||||
("domain_body", "신체", ["잠", "식욕", "몸", "두통", "피곤", "숨"], "client"),
|
||||
],
|
||||
),
|
||||
(
|
||||
"cognitive_triad_emotions",
|
||||
"인지삼제·1/2차 감정",
|
||||
[
|
||||
("triad_self", "자기", ["나는", "내가", "나 자신", "스스로"], "client"),
|
||||
("triad_world", "타인·세계", ["사람", "세상", "학교", "가족", "친구"], "client"),
|
||||
("triad_future", "미래", ["앞으로", "미래", "계속", "나중"], "client"),
|
||||
("primary_emotion", "1차 감정", ["불안", "슬프", "무섭", "외롭", "걱정"], "client"),
|
||||
("secondary_emotion", "2차 감정", ["화", "짜증", "수치", "죄책", "부끄"], "client"),
|
||||
],
|
||||
),
|
||||
(
|
||||
"protective_barrier_quadrants",
|
||||
"보호·방해 4사분면",
|
||||
[
|
||||
("internal_protective", "내적 보호요인", ["해보고", "버텼", "노력", "원", "견뎠"], None),
|
||||
("internal_barrier", "내적 방해요인", ["못", "두려", "불안", "회피", "걱정"], "client"),
|
||||
("external_protective", "외적 보호요인", ["친구", "가족", "상담", "교수", "도움"], "client"),
|
||||
("external_barrier", "외적 방해요인", ["갈등", "압박", "비난", "스트레스", "혼자"], "client"),
|
||||
],
|
||||
),
|
||||
(
|
||||
"biopsychosocial_goals",
|
||||
"생물·심리·사회 목표",
|
||||
[
|
||||
("bio_goal", "생물", ["잠", "식사", "운동", "몸", "피곤"], "client"),
|
||||
("psy_goal", "심리", ["생각", "감정", "불안", "연습", "조절"], None),
|
||||
("social_goal", "사회", ["관계", "대화", "연락", "도움", "친구"], None),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
||||
sections = [
|
||||
_worksheet_section(
|
||||
|
|
@ -927,7 +937,7 @@ def case_worksheet_from_turns(turns: list[ReviewTurn]) -> ReviewCaseWorksheet:
|
|||
fallback_client,
|
||||
fallback_learner,
|
||||
)
|
||||
for key, title, specs in section_specs
|
||||
for key, title, specs in CASE_WORKSHEET_SECTION_SPECS
|
||||
]
|
||||
return ReviewCaseWorksheet(
|
||||
status="draft_from_transcript",
|
||||
|
|
|
|||
93
apps/api/app/test_case_worksheet_rubric.py
Normal file
93
apps/api/app/test_case_worksheet_rubric.py
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import copy
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from jsonschema import Draft202012Validator
|
||||
except ModuleNotFoundError: # pragma: no cover - optional test helper dependency
|
||||
Draft202012Validator = None
|
||||
|
||||
from app.services.case_worksheet_rubric import load_rubric, validate_rubric
|
||||
from app.session_read_model import case_worksheet_template_item_keys
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[3]
|
||||
RUBRIC_PATH = REPO_ROOT / "data" / "rubrics" / "case-worksheet-rubric.json"
|
||||
SCHEMA_PATH = REPO_ROOT / "data" / "rubrics" / "case-worksheet-rubric.schema.json"
|
||||
SCRIPT_PATH = REPO_ROOT / "scripts" / "check-case-worksheet-rubric.py"
|
||||
|
||||
|
||||
class CaseWorksheetRubricTests(unittest.TestCase):
|
||||
def test_scaffold_matches_generated_worksheet_keys_without_enabling_scoring(self) -> None:
|
||||
rubric = load_rubric(RUBRIC_PATH)
|
||||
report = validate_rubric(rubric, expected_item_keys=case_worksheet_template_item_keys())
|
||||
|
||||
self.assertTrue(report["passed"], report)
|
||||
self.assertEqual(report["schema_version"], "vignette.case_worksheet_rubric.v1")
|
||||
self.assertEqual(report["status"], "scaffold_only")
|
||||
self.assertFalse(report["scoring_enabled"])
|
||||
self.assertEqual(report["sections_total"], 5)
|
||||
self.assertEqual(report["items_total"], 28)
|
||||
self.assertGreaterEqual(len(report["warnings"]), 20)
|
||||
self._validate_with_schema(rubric, SCHEMA_PATH)
|
||||
|
||||
def test_scoring_requires_clinical_approval(self) -> None:
|
||||
rubric = load_rubric(RUBRIC_PATH)
|
||||
draft = copy.deepcopy(rubric)
|
||||
draft["status"] = "draft"
|
||||
draft["scoring_enabled"] = True
|
||||
|
||||
report = validate_rubric(draft, expected_item_keys=case_worksheet_template_item_keys())
|
||||
|
||||
self.assertFalse(report["passed"])
|
||||
self.assertIn("scoring_enabled requires status=approved", report["errors"])
|
||||
|
||||
def test_missing_worksheet_item_fails_validation(self) -> None:
|
||||
rubric = load_rubric(RUBRIC_PATH)
|
||||
broken = copy.deepcopy(rubric)
|
||||
broken["sections"][0]["items"] = broken["sections"][0]["items"][1:]
|
||||
|
||||
report = validate_rubric(broken, expected_item_keys=case_worksheet_template_item_keys())
|
||||
|
||||
self.assertFalse(report["passed"])
|
||||
self.assertIn("exploration_11: missing worksheet item: presenting_complaint", report["errors"])
|
||||
|
||||
def test_cli_reports_json(self) -> None:
|
||||
completed = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
"-X",
|
||||
"utf8",
|
||||
str(SCRIPT_PATH),
|
||||
"--rubric",
|
||||
str(RUBRIC_PATH),
|
||||
"--json",
|
||||
],
|
||||
cwd=str(REPO_ROOT),
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
report = json.loads(completed.stdout)
|
||||
self.assertTrue(report["passed"], report)
|
||||
self.assertEqual(report["items_total"], 28)
|
||||
self.assertFalse(report["scoring_enabled"])
|
||||
self.assertEqual(report["rubric_path"], "data/rubrics/case-worksheet-rubric.json")
|
||||
self.assertRegex(report["content_sha256"], r"^[a-f0-9]{64}$")
|
||||
|
||||
def _validate_with_schema(self, instance: object, schema_path: Path) -> None:
|
||||
schema = json.loads(schema_path.read_text(encoding="utf-8"))
|
||||
self.assertEqual(schema.get("$schema"), "https://json-schema.org/draft/2020-12/schema")
|
||||
if Draft202012Validator is None:
|
||||
return
|
||||
Draft202012Validator.check_schema(schema)
|
||||
Draft202012Validator(schema).validate(instance)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
@ -5,14 +5,46 @@ import unittest
|
|||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
try:
|
||||
from jsonschema import Draft202012Validator
|
||||
except ModuleNotFoundError: # pragma: no cover - optional test helper dependency
|
||||
Draft202012Validator = None
|
||||
|
||||
from app.services import guardrail
|
||||
from app.services.pii_masking_eval import evaluate_fixture, load_cases
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[3]
|
||||
FIXTURE_PATH = REPO_ROOT / "data" / "privacy" / "pii-masking-ko-fixtures.json"
|
||||
INPUT_SCHEMA_PATH = REPO_ROOT / "data" / "privacy" / "pii-masking-eval-input.schema.json"
|
||||
REPORT_SCHEMA_PATH = REPO_ROOT / "data" / "privacy" / "pii-masking-eval-report.schema.json"
|
||||
SCRIPT_PATH = REPO_ROOT / "scripts" / "evaluate-pii-masking.py"
|
||||
|
||||
EXPECTED_CATEGORIES = {
|
||||
"contact",
|
||||
"name",
|
||||
"national_id",
|
||||
"negative_control",
|
||||
"organization",
|
||||
"quasi_identifier",
|
||||
}
|
||||
RAW_IDENTIFIERS = (
|
||||
"김서연",
|
||||
"박민수",
|
||||
"최하늘",
|
||||
"한신대학교",
|
||||
"상담심리학과",
|
||||
"마음봄상담센터",
|
||||
"새봄병원",
|
||||
"010-1234-5678",
|
||||
"seoyeon@example.com",
|
||||
"990101-1234567",
|
||||
"123456789012",
|
||||
"2001년 4월 18일",
|
||||
"서울시 강남구 역삼동",
|
||||
"1200원",
|
||||
)
|
||||
|
||||
|
||||
class PiiMaskingEvalTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
|
|
@ -27,22 +59,57 @@ class PiiMaskingEvalTests(unittest.TestCase):
|
|||
def test_fixture_cases_are_valid_json_list(self) -> None:
|
||||
cases = load_cases(FIXTURE_PATH)
|
||||
|
||||
self.assertGreaterEqual(len(cases), 5)
|
||||
self.assertEqual(len(cases), 15)
|
||||
self.assertTrue(all(case.get("id") for case in cases))
|
||||
self.assertTrue(all(case.get("text") for case in cases))
|
||||
self.assertTrue(all(case.get("locale") == "ko-KR" for case in cases))
|
||||
self.assertTrue(all(case.get("source") == "synthetic" for case in cases))
|
||||
self.assertTrue(all("expected_entities" in case for case in cases))
|
||||
self.assertTrue(all("forbidden_substrings" in case for case in cases))
|
||||
self.assertEqual({case["category"] for case in cases}, EXPECTED_CATEGORIES)
|
||||
self._validate_with_schema(cases, INPUT_SCHEMA_PATH)
|
||||
|
||||
def test_ko_name_org_fixture_passes_without_raw_identifier_leak(self) -> None:
|
||||
report = evaluate_fixture(FIXTURE_PATH)
|
||||
|
||||
self.assertEqual(report["schema_version"], "vignette.pii_masking_eval_report.v1")
|
||||
self.assertEqual(report["input_schema_version"], "vignette.pii_masking_eval_input.v1")
|
||||
self.assertEqual(report["run_mode"], "technical_dry_run")
|
||||
self.assertEqual(report["data_source"], "local_fixture")
|
||||
self.assertFalse(report["evidence_text_included"])
|
||||
self.assertTrue(report["passed"], report)
|
||||
self.assertEqual(report["cases_total"], 15)
|
||||
self.assertEqual(report["cases_passed"], 15)
|
||||
self.assertEqual(report["cases_failed"], 0)
|
||||
self.assertEqual(report["expected_entity_recall"], 1.0)
|
||||
self.assertEqual(report["forbidden_substring_removal"], 1.0)
|
||||
self.assertEqual(report["unexpected_entity_violations"], 0)
|
||||
self.assertEqual(report["by_source"]["synthetic"]["cases_passed"], 15)
|
||||
self.assertEqual(set(report["by_category"]), EXPECTED_CATEGORIES)
|
||||
self.assertEqual(report["by_severity"]["critical"]["cases_passed"], 2)
|
||||
for result in report["results"]:
|
||||
self.assertNotIn("masked_text", result)
|
||||
self.assertNotIn("forbidden_remaining", result)
|
||||
self.assertIn("forbidden_remaining_count", result)
|
||||
self._validate_with_schema(report, REPORT_SCHEMA_PATH)
|
||||
blob = json.dumps(report, ensure_ascii=False)
|
||||
for raw in ("김서연", "박민수", "한신대학교", "상담심리학과", "마음봄상담센터"):
|
||||
for raw in RAW_IDENTIFIERS:
|
||||
self.assertNotIn(raw, blob)
|
||||
|
||||
def test_natural_language_name_contexts_are_masked_without_label_false_positive(self) -> None:
|
||||
cases = {
|
||||
"제 이름은 김서연입니다.": "제 이름은 [NAME]입니다.",
|
||||
"보호자 이름은 박민수입니다.": "보호자 이름은 [NAME]입니다.",
|
||||
"저는 최하늘입니다.": "저는 [NAME]입니다.",
|
||||
"김서연입니다.": "[NAME]입니다.",
|
||||
"이름은 중요하지 않고 상담 내용만 이야기하고 싶어요.": "이름은 중요하지 않고 상담 내용만 이야기하고 싶어요.",
|
||||
}
|
||||
|
||||
for raw, expected in cases.items():
|
||||
with self.subTest(raw=raw):
|
||||
result = guardrail.mask_pii(raw)
|
||||
self.assertEqual(result.text_masked, expected)
|
||||
|
||||
def test_cli_reports_json_and_nonzero_gate_shape(self) -> None:
|
||||
completed = subprocess.run(
|
||||
[
|
||||
|
|
@ -63,7 +130,19 @@ class PiiMaskingEvalTests(unittest.TestCase):
|
|||
|
||||
report = json.loads(completed.stdout)
|
||||
self.assertTrue(report["passed"])
|
||||
self.assertEqual(report["cases_total"], 5)
|
||||
self.assertEqual(report["cases_total"], 15)
|
||||
self.assertFalse(report["evidence_text_included"])
|
||||
self.assertEqual(set(report["by_category"]), EXPECTED_CATEGORIES)
|
||||
self.assertTrue(all("masked_text" not in result for result in report["results"]))
|
||||
self._validate_with_schema(report, REPORT_SCHEMA_PATH)
|
||||
|
||||
def _validate_with_schema(self, instance: object, schema_path: Path) -> None:
|
||||
schema = json.loads(schema_path.read_text(encoding="utf-8"))
|
||||
self.assertEqual(schema.get("$schema"), "https://json-schema.org/draft/2020-12/schema")
|
||||
if Draft202012Validator is None:
|
||||
return
|
||||
Draft202012Validator.check_schema(schema)
|
||||
Draft202012Validator(schema).validate(instance)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -376,6 +376,10 @@ test.describe("layout visual gate @single-run", () => {
|
|||
await gateScreen(page, "persona-studio", async () => {
|
||||
await expect(page.locator(".ps-layout")).toBeVisible({ timeout: 15_000 });
|
||||
await expect(page.getByText("내담자 설계·검수 작업면")).toBeVisible();
|
||||
await page.getByRole("tab", { name: "프롬프트" }).click();
|
||||
const promptPreview = page.getByLabel("프롬프트 미리보기");
|
||||
await expect(promptPreview).toBeVisible();
|
||||
await expect(promptPreview.getByRole("textbox")).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -329,6 +329,108 @@ test.describe("teacher console", () => {
|
|||
await expectNoHorizontalOverflow(page);
|
||||
});
|
||||
|
||||
test("saves persona studio list rows as structured arrays @single-run", async ({ page }) => {
|
||||
type PersonaDraftSavePayload = {
|
||||
ccd: { automatic_thought: string[] };
|
||||
code: string;
|
||||
difficulty: string;
|
||||
display_name: string;
|
||||
is_synthetic: boolean;
|
||||
source_provenance: unknown;
|
||||
theory_target: string;
|
||||
triggers: { forbidden: string[]; sore_spots: string[] };
|
||||
};
|
||||
let savedPayload: PersonaDraftSavePayload | null = null;
|
||||
|
||||
await signInAsTeacher(page);
|
||||
await page.route("**/api/personas", (route) => {
|
||||
if (route.request().method() !== "GET") return route.fallback();
|
||||
return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify([]) });
|
||||
});
|
||||
await page.route("**/api/personas/review", (route) => {
|
||||
if (route.request().method() !== "GET") return route.fallback();
|
||||
return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify([]) });
|
||||
});
|
||||
await page.route("**/api/personas/drafts", async (route) => {
|
||||
if (route.request().method() !== "POST") return route.fallback();
|
||||
savedPayload = route.request().postDataJSON() as PersonaDraftSavePayload;
|
||||
await route.fulfill({
|
||||
status: 201,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
persona_id: "00000000-0000-0000-0000-000000000703",
|
||||
code: savedPayload.code,
|
||||
version: 1,
|
||||
status: "draft",
|
||||
display_name: savedPayload.display_name,
|
||||
difficulty: savedPayload.difficulty,
|
||||
theory_target: savedPayload.theory_target,
|
||||
source_provenance: savedPayload.source_provenance,
|
||||
is_synthetic: savedPayload.is_synthetic,
|
||||
created_at: "2026-06-28T00:00:00Z",
|
||||
approved_at: null,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto("/teach/personas");
|
||||
await page.getByLabel("표시 이름").fill("항목형 페르소나");
|
||||
|
||||
await page.getByRole("tab", { name: "임상" }).click();
|
||||
const automaticThoughts = page.locator(".ps-list-field").filter({ hasText: "자동사고" });
|
||||
await automaticThoughts.getByRole("textbox", { name: "자동사고 1", exact: true }).fill("삭제될 자동사고");
|
||||
await automaticThoughts.getByRole("button", { name: "항목 추가" }).click();
|
||||
await automaticThoughts.getByRole("textbox", { name: "자동사고 2", exact: true }).fill("남길 자동사고");
|
||||
await automaticThoughts.getByRole("button", { name: "자동사고 1 삭제" }).click();
|
||||
|
||||
await page.getByRole("tab", { name: "안전" }).click();
|
||||
const forbidden = page.locator(".ps-list-field").filter({ hasText: "상담자 금기" });
|
||||
await forbidden.getByRole("textbox", { name: "상담자 금기 1", exact: true }).fill("네가 예민한 거라고 단정");
|
||||
|
||||
await page.getByRole("button", { name: "초안 저장" }).click();
|
||||
await expect(page.getByText("P1 v1 초안을 저장했습니다.")).toBeVisible();
|
||||
|
||||
expect(savedPayload).toBeTruthy();
|
||||
expect(savedPayload?.ccd.automatic_thought).toEqual(["남길 자동사고"]);
|
||||
expect(savedPayload?.triggers.forbidden).toEqual(["네가 예민한 거라고 단정"]);
|
||||
expect(savedPayload?.triggers.sore_spots).toEqual([]);
|
||||
await expectNoHorizontalOverflow(page);
|
||||
});
|
||||
|
||||
test("renders persona prompt preview as labeled sections without raw JSON @single-run", async ({ page }) => {
|
||||
await signInAsTeacher(page);
|
||||
await page.route("**/api/personas", (route) => {
|
||||
if (route.request().method() !== "GET") return route.fallback();
|
||||
return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify([]) });
|
||||
});
|
||||
await page.route("**/api/personas/review", (route) => {
|
||||
if (route.request().method() !== "GET") return route.fallback();
|
||||
return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify([]) });
|
||||
});
|
||||
|
||||
await page.goto("/teach/personas");
|
||||
await page.getByLabel("표시 이름").fill("프롬프트 검토 페르소나");
|
||||
await page.getByRole("tab", { name: "임상" }).click();
|
||||
const automaticThoughts = page.locator(".ps-list-field").filter({ hasText: "자동사고" });
|
||||
await automaticThoughts.getByRole("textbox", { name: "자동사고 1", exact: true }).fill("말하면 더 이상하게 볼 거야");
|
||||
await page.getByRole("tab", { name: "안전" }).click();
|
||||
const forbidden = page.locator(".ps-list-field").filter({ hasText: "상담자 금기" });
|
||||
await forbidden.getByRole("textbox", { name: "상담자 금기 1", exact: true }).fill("네가 예민한 거라고 단정");
|
||||
|
||||
await page.getByRole("tab", { name: "프롬프트" }).click();
|
||||
const promptPreview = page.getByLabel("프롬프트 미리보기");
|
||||
await expect(promptPreview.getByRole("heading", { name: /L1 페르소나 카드/ })).toBeVisible();
|
||||
await expect(promptPreview.getByText("자동사고")).toBeVisible();
|
||||
await expect(promptPreview.getByText("말하면 더 이상하게 볼 거야")).toBeVisible();
|
||||
await expect(promptPreview.getByText("상담자 금기")).toBeVisible();
|
||||
await expect(promptPreview.getByText("네가 예민한 거라고 단정")).toBeVisible();
|
||||
await expect(promptPreview.getByRole("textbox")).toHaveCount(0);
|
||||
await expect(promptPreview).not.toContainText('"automatic_thought"');
|
||||
await expect(promptPreview).not.toContainText('"forbidden"');
|
||||
await expect(promptPreview).not.toContainText("{");
|
||||
await expectNoHorizontalOverflow(page);
|
||||
});
|
||||
|
||||
test("archives an approved persona from persona studio without layout drift @single-run", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
const personaId = "00000000-0000-0000-0000-000000000702";
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@
|
|||
/>
|
||||
<meta name="robots" content="index,follow,max-snippet:160,max-image-preview:large" />
|
||||
<link rel="canonical" href="https://vignette.chanpaca.net/" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="shortcut icon" href="/favicon.svg" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:locale" content="ko_KR" />
|
||||
<meta property="og:site_name" content="Vignette" />
|
||||
|
|
|
|||
5
apps/web/public/favicon.svg
Normal file
5
apps/web/public/favicon.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Vignette">
|
||||
<rect width="64" height="64" rx="14" fill="#f6f1e8"/>
|
||||
<path d="M14 14h36L35.7 50H27L14 14Z" fill="#2f6f73"/>
|
||||
<path d="M24.2 20h14.9l-7.2 19.1L24.2 20Z" fill="#c46d4a"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 276 B |
|
|
@ -36,6 +36,7 @@ import { Mouth } from "./Mouth";
|
|||
import { live2dModel3Path, live2dModelForPersonaCode, live2dMotionForExpression } from "./live2dModel";
|
||||
import { useExpressionTransition } from "./useExpressionTransition";
|
||||
import { RasterBust } from "./RasterBust";
|
||||
import "./client-avatar.css";
|
||||
|
||||
/* ── 공개 타입 재노출 (기존 import 경로 호환) ──────────────────────────
|
||||
Session.tsx 등이 ClientAvatar 모듈에서 타입을 가져갈 수 있으므로 유지. */
|
||||
|
|
@ -82,7 +83,6 @@ function prefersReducedMotion(): boolean {
|
|||
window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true
|
||||
);
|
||||
}
|
||||
|
||||
/** prefers-reduced-motion 을 반응형으로 구독 */
|
||||
function useReducedMotion(): boolean {
|
||||
const [reduced, setReduced] = useState<boolean>(prefersReducedMotion);
|
||||
|
|
@ -299,7 +299,6 @@ export function ClientAvatar({
|
|||
data-realism={realism} /* 사실성은 데이터로만 유지(시각 표식 비노출, §4.6) */
|
||||
aria-label={`교육용 가상 내담자: ${persona.label}, ${STATE_TEXT[state]}, ${expressionLabel}`}
|
||||
>
|
||||
<style>{AVATAR_CSS}</style>
|
||||
|
||||
{/* 상단 라벨 — 실존 인물 오인 차단(상시, §4.1) */}
|
||||
{showCaption ? (
|
||||
|
|
@ -399,60 +398,3 @@ export function ClientAvatar({
|
|||
</figure>
|
||||
);
|
||||
}
|
||||
|
||||
const AVATAR_CSS = `
|
||||
.vg-avatar{display:flex;flex-direction:column;align-items:center;gap:var(--sp-3);margin:0;}
|
||||
.vg-avatar__label{
|
||||
display:inline-flex;align-items:center;gap:7px;
|
||||
font-family:var(--font-num);font-size:var(--fs-xs);font-weight:600;letter-spacing:0.06em;
|
||||
color:var(--text-muted);text-transform:none;
|
||||
}
|
||||
.vg-avatar__label-dot{width:6px;height:6px;border-radius:50%;background:var(--clay);flex:none;}
|
||||
.vg-avatar__stage{
|
||||
position:relative;width:100%;border-radius:50%;
|
||||
display:flex;align-items:center;justify-content:center;overflow:hidden;
|
||||
background:
|
||||
radial-gradient(circle at 50% 38%, rgba(251,250,248,.98) 0%, rgba(246,240,236,.9) 42%, rgba(238,244,242,.76) 64%, rgba(145,200,189,.22) 84%, rgba(30,39,36,.18) 100%);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(251,250,248,.28),
|
||||
inset 0 -24px 40px rgba(28,42,42,.13);
|
||||
}
|
||||
.vg-avatar__aura{
|
||||
position:absolute;inset:-12%;border-radius:50%;pointer-events:none;
|
||||
animation:vgAuraBreathe 6s var(--ease-in-out) infinite;
|
||||
}
|
||||
.vg-avatar__aura.is-reduced{animation:none;}
|
||||
@keyframes vgAuraBreathe{0%,100%{opacity:.85;transform:scale(1)}50%{opacity:1;transform:scale(1.04)}}
|
||||
.vg-avatar__svg{position:relative;z-index:1;display:block;transition:opacity var(--dur-base) var(--ease-out);}
|
||||
/* ── 래스터(Live2D식) 렌더: 레이어 합성 흉상 ── */
|
||||
.vg-raster{position:absolute;inset:0;z-index:1;pointer-events:none;will-change:transform;}
|
||||
.vg-raster__layer{
|
||||
position:absolute;left:50%;top:var(--vg-raster-top,-15%);
|
||||
height:var(--vg-raster-h,136%);width:auto;max-width:none;
|
||||
transform:translateX(-50%);object-fit:contain;
|
||||
user-select:none;-webkit-user-drag:none;opacity:0;
|
||||
}
|
||||
.vg-raster__layer--shoulders{opacity:1;z-index:1;}
|
||||
.vg-raster__layer--neck{opacity:1;z-index:2;}
|
||||
.vg-raster__layer--hairback{opacity:1;z-index:3;}
|
||||
.vg-raster__layer--ear{opacity:1;z-index:4;}
|
||||
.vg-raster__layer--face{opacity:1;z-index:5;}
|
||||
.vg-raster__layer--forehead{opacity:1;z-index:6;}
|
||||
.vg-raster__layer--hair{opacity:1;z-index:7;will-change:transform;}
|
||||
.vg-raster__layer--bangs{opacity:1;z-index:8;will-change:transform;}
|
||||
.vg-raster__layer--brow{z-index:9;will-change:opacity,transform;}
|
||||
.vg-raster__layer--eyes{z-index:10;}
|
||||
.vg-raster__layer--eyelid{z-index:11;}
|
||||
.vg-raster__layer--nose{opacity:1;z-index:12;}
|
||||
.vg-raster__layer--mouth{z-index:13;}
|
||||
.vg-raster__layer--mouthopen{z-index:14;}
|
||||
.vg-raster__layer--base{opacity:1;z-index:1;}
|
||||
.vg-raster__layer--upperface{z-index:2;}
|
||||
.vg-avatar__meta{display:flex;flex-direction:column;align-items:center;gap:3px;text-align:center;}
|
||||
.vg-avatar__persona{font-size:var(--fs-sm);font-weight:600;color:var(--text-strong);}
|
||||
.vg-avatar__state{font-size:var(--fs-xs);color:var(--text-muted);}
|
||||
.vg-avatar__state b{font-weight:600;color:var(--text-body);}
|
||||
@media (prefers-reduced-motion: reduce){
|
||||
.vg-avatar__aura{animation:none;}
|
||||
}
|
||||
`;
|
||||
|
|
|
|||
54
apps/web/src/components/avatar/client-avatar.css
Normal file
54
apps/web/src/components/avatar/client-avatar.css
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
.vg-avatar{display:flex;flex-direction:column;align-items:center;gap:var(--sp-3);margin:0;}
|
||||
.vg-avatar__label{
|
||||
display:inline-flex;align-items:center;gap:7px;
|
||||
font-family:var(--font-num);font-size:var(--fs-xs);font-weight:600;letter-spacing:0.06em;
|
||||
color:var(--text-muted);text-transform:none;
|
||||
}
|
||||
.vg-avatar__label-dot{width:6px;height:6px;border-radius:50%;background:var(--clay);flex:none;}
|
||||
.vg-avatar__stage{
|
||||
position:relative;width:100%;border-radius:50%;
|
||||
display:flex;align-items:center;justify-content:center;overflow:hidden;
|
||||
background:
|
||||
radial-gradient(circle at 50% 38%, rgba(251,250,248,.98) 0%, rgba(246,240,236,.9) 42%, rgba(238,244,242,.76) 64%, rgba(145,200,189,.22) 84%, rgba(30,39,36,.18) 100%);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(251,250,248,.28),
|
||||
inset 0 -24px 40px rgba(28,42,42,.13);
|
||||
}
|
||||
.vg-avatar__aura{
|
||||
position:absolute;inset:-12%;border-radius:50%;pointer-events:none;
|
||||
animation:vgAuraBreathe 6s var(--ease-in-out) infinite;
|
||||
}
|
||||
.vg-avatar__aura.is-reduced{animation:none;}
|
||||
@keyframes vgAuraBreathe{0%,100%{opacity:.85;transform:scale(1)}50%{opacity:1;transform:scale(1.04)}}
|
||||
.vg-avatar__svg{position:relative;z-index:1;display:block;transition:opacity var(--dur-base) var(--ease-out);}
|
||||
/* ── 래스터(Live2D식) 렌더: 레이어 합성 흉상 ── */
|
||||
.vg-raster{position:absolute;inset:0;z-index:1;pointer-events:none;will-change:transform;}
|
||||
.vg-raster__layer{
|
||||
position:absolute;left:50%;top:var(--vg-raster-top,-15%);
|
||||
height:var(--vg-raster-h,136%);width:auto;max-width:none;
|
||||
transform:translateX(-50%);object-fit:contain;
|
||||
user-select:none;-webkit-user-drag:none;opacity:0;
|
||||
}
|
||||
.vg-raster__layer--shoulders{opacity:1;z-index:1;}
|
||||
.vg-raster__layer--neck{opacity:1;z-index:2;}
|
||||
.vg-raster__layer--hairback{opacity:1;z-index:3;}
|
||||
.vg-raster__layer--ear{opacity:1;z-index:4;}
|
||||
.vg-raster__layer--face{opacity:1;z-index:5;}
|
||||
.vg-raster__layer--forehead{opacity:1;z-index:6;}
|
||||
.vg-raster__layer--hair{opacity:1;z-index:7;will-change:transform;}
|
||||
.vg-raster__layer--bangs{opacity:1;z-index:8;will-change:transform;}
|
||||
.vg-raster__layer--brow{z-index:9;will-change:opacity,transform;}
|
||||
.vg-raster__layer--eyes{z-index:10;}
|
||||
.vg-raster__layer--eyelid{z-index:11;}
|
||||
.vg-raster__layer--nose{opacity:1;z-index:12;}
|
||||
.vg-raster__layer--mouth{z-index:13;}
|
||||
.vg-raster__layer--mouthopen{z-index:14;}
|
||||
.vg-raster__layer--base{opacity:1;z-index:1;}
|
||||
.vg-raster__layer--upperface{z-index:2;}
|
||||
.vg-avatar__meta{display:flex;flex-direction:column;align-items:center;gap:3px;text-align:center;}
|
||||
.vg-avatar__persona{font-size:var(--fs-sm);font-weight:600;color:var(--text-strong);}
|
||||
.vg-avatar__state{font-size:var(--fs-xs);color:var(--text-muted);}
|
||||
.vg-avatar__state b{font-weight:600;color:var(--text-body);}
|
||||
@media (prefers-reduced-motion: reduce){
|
||||
.vg-avatar__aura{animation:none;}
|
||||
}
|
||||
|
|
@ -18,6 +18,7 @@ export type IconName =
|
|||
| "chevron-right"
|
||||
| "chevron-left"
|
||||
| "check"
|
||||
| "plus"
|
||||
| "share"
|
||||
| "mic"
|
||||
| "mic-off"
|
||||
|
|
@ -94,6 +95,12 @@ 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" />,
|
||||
plus: (
|
||||
<>
|
||||
<line x1="12" y1="5" x2="12" y2="19" />
|
||||
<line x1="5" y1="12" x2="19" y2="12" />
|
||||
</>
|
||||
),
|
||||
share: (
|
||||
<>
|
||||
<circle cx="18" cy="5" r="3" />
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,8 +1,10 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { Icon } from "../components/ui/Icon";
|
||||
import { roleHomePath, useAuth, type Role } from "../lib/auth";
|
||||
import { apiUrl, authApi, type AuthConfigResponse } from "../lib/api";
|
||||
import { LoginBrand } from "./login/LoginBrand";
|
||||
import { LoginPanel, type LoginRoleOption } from "./login/LoginPanel";
|
||||
import "./login/login.css";
|
||||
|
||||
const OAUTH_NOT_CONFIGURED_MESSAGE =
|
||||
"Google 로그인이 아직 연결되지 않았습니다. 관리자에게 OAuth 클라이언트 설정을 요청하세요.";
|
||||
|
|
@ -57,7 +59,7 @@ function oauthMessage(reason: string | null): string | null {
|
|||
return OAUTH_FAILED_MESSAGE;
|
||||
}
|
||||
|
||||
const ROLE_OPTIONS: { role: Role; label: string; desc: string; dotClass: string }[] = [
|
||||
const ROLE_OPTIONS: LoginRoleOption[] = [
|
||||
{ role: "learner", label: "학습자", desc: "연습 공간으로 이동", dotClass: "learner" },
|
||||
{ role: "teacher", label: "교수자", desc: "담당 학습자 관리", dotClass: "teacher" },
|
||||
{ role: "admin", label: "관리자", desc: "운영 설정과 감사", dotClass: "admin" },
|
||||
|
|
@ -143,6 +145,11 @@ export default function Login() {
|
|||
: allowedDomains[0]
|
||||
? "승인된 Google 계정"
|
||||
: "관리자 설정 필요";
|
||||
const oauthStatusMessage = devOAuthUnavailable
|
||||
? LOCAL_OAUTH_UNAVAILABLE_MESSAGE
|
||||
: oauthChecking
|
||||
? "Google 로그인 설정을 확인하는 중입니다."
|
||||
: "Google 로그인이 아직 연결되지 않았습니다. 관리자에게 OAuth 클라이언트 설정을 요청하세요.";
|
||||
|
||||
const startOAuth = () => {
|
||||
if (!oauthReady) {
|
||||
|
|
@ -182,485 +189,28 @@ export default function Login() {
|
|||
|
||||
return (
|
||||
<main className="lg-root">
|
||||
<style>{LOGIN_CSS}</style>
|
||||
|
||||
<section className="lg-brand" aria-label="Vignette">
|
||||
<div className="lg-wordmark">
|
||||
<span className="lg-mark" aria-hidden="true">
|
||||
<svg viewBox="0 0 26 26" width={26} height={26} fill="none">
|
||||
<circle cx="13" cy="13" r="11" stroke="currentColor" strokeWidth="2" />
|
||||
<path
|
||||
d="M8 14.5c1.4 1.7 3 2.5 5 2.5s3.6-.8 5-2.5"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<circle cx="13" cy="9" r="1.6" fill="var(--clay)" />
|
||||
</svg>
|
||||
</span>
|
||||
<span>Vignette</span>
|
||||
</div>
|
||||
|
||||
<div className="lg-copy">
|
||||
<span className="lg-kicker">
|
||||
<span className="d" aria-hidden="true" />
|
||||
상담 시뮬레이션 학습
|
||||
</span>
|
||||
<h1><span className="lg-highlight">실제 계정으로 들어가고</span>, <br/>실제 회기만 남깁니다.</h1>
|
||||
<p>
|
||||
Vignette는 상담 연습, 교수자 피드백, 운영 관리 권한을 안전한 로그인 세션으로 분리합니다.
|
||||
브라우저에는 인증 토큰을 저장하지 않습니다.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="lg-policy">
|
||||
<span>
|
||||
<Icon name="shield" size={17} />
|
||||
허용 도메인
|
||||
</span>
|
||||
{allowedDomains.length ? (
|
||||
allowedDomains.map((domain) => <b key={domain}>{domain}</b>)
|
||||
) : (
|
||||
<b>{oauthChecking ? "확인 중" : "설정 필요"}</b>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="lg-enter" aria-label="로그인">
|
||||
<div className="lg-panel">
|
||||
<span className="lg-kicker">
|
||||
<span className="d" aria-hidden="true" />
|
||||
계정으로 시작
|
||||
</span>
|
||||
<h2>로그인</h2>
|
||||
<p className="lg-lead">
|
||||
학교 또는 승인된 Google 계정으로 접속하면 역할과 코호트 권한을 확인합니다.
|
||||
</p>
|
||||
|
||||
<div className="lg-actions">
|
||||
<button
|
||||
className="lg-obtn primary"
|
||||
type="button"
|
||||
onClick={startOAuth}
|
||||
disabled={!oauthReady}
|
||||
>
|
||||
<span className="ic">
|
||||
<Icon name="school" size={19} strokeWidth={1.8} />
|
||||
</span>
|
||||
<span className="txt">
|
||||
학교 Google 계정으로 계속
|
||||
<span className="sub">{primaryDomainLabel}</span>
|
||||
</span>
|
||||
<Icon name="chevron-right" size={18} strokeWidth={2} />
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="lg-obtn secondary"
|
||||
type="button"
|
||||
onClick={startOAuth}
|
||||
disabled={!oauthReady}
|
||||
>
|
||||
<span className="ic">
|
||||
<Icon name="google" size={19} />
|
||||
</span>
|
||||
<span className="txt">
|
||||
Google 계정으로 계속
|
||||
<span className="sub">{secondaryDomainLabel}</span>
|
||||
</span>
|
||||
<Icon name="chevron-right" size={18} strokeWidth={2} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{!oauthReady ? (
|
||||
<div className="lg-config" role="status">
|
||||
<Icon name={authConfigError ? "alert" : "info"} size={17} />
|
||||
<span>
|
||||
{devOAuthUnavailable
|
||||
? LOCAL_OAUTH_UNAVAILABLE_MESSAGE
|
||||
: oauthChecking
|
||||
? "Google 로그인 설정을 확인하는 중입니다."
|
||||
: "Google 로그인이 아직 연결되지 않았습니다. 관리자에게 OAuth 클라이언트 설정을 요청하세요."}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{devLoginReady ? (
|
||||
<div className="lg-dev">
|
||||
<div className="lg-devhead">
|
||||
<span>로컬 테스트</span>
|
||||
<small>보안 로그인 세션 사용</small>
|
||||
</div>
|
||||
<div className="lg-rolepick" role="radiogroup" aria-label="로컬 테스트 역할">
|
||||
{ROLE_OPTIONS.map((opt) => (
|
||||
<button
|
||||
key={opt.role}
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={selected === opt.role}
|
||||
className={`lg-roleopt ${selected === opt.role ? "is-sel" : ""}`}
|
||||
onClick={() => setSelected(opt.role)}
|
||||
>
|
||||
<span className={`d ${opt.dotClass}`} aria-hidden="true" />
|
||||
<span>
|
||||
<b>{opt.label}</b>
|
||||
<small>{opt.desc}</small>
|
||||
</span>
|
||||
{selected === opt.role ? <Icon name="check" size={15} /> : null}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
className="lg-devbtn"
|
||||
type="button"
|
||||
onClick={() => void enterDev(selected)}
|
||||
disabled={pending}
|
||||
>
|
||||
{pending ? "테스트 계정 확인 중" : "로컬 테스트 계정으로 계속"}
|
||||
</button>
|
||||
{loginError ? (
|
||||
<p className="lg-error">
|
||||
{loginError}
|
||||
{loginErrorReason ? <small>오류 코드: {loginErrorReason}</small> : null}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!devLoginReady && loginError ? (
|
||||
<p className="lg-error">
|
||||
{loginError}
|
||||
{loginErrorReason ? <small>오류 코드: {loginErrorReason}</small> : null}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<p className="lg-note">
|
||||
교육용 비치료 연구 도구입니다. 실제 치료, 진단, 위기 개입을 대체하지 않습니다.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
<LoginBrand allowedDomains={allowedDomains} oauthChecking={oauthChecking} />
|
||||
<LoginPanel
|
||||
oauth={{
|
||||
ready: oauthReady,
|
||||
primaryDomainLabel,
|
||||
secondaryDomainLabel,
|
||||
statusIcon: authConfigError ? "alert" : "info",
|
||||
statusMessage: oauthStatusMessage,
|
||||
onStart: startOAuth,
|
||||
}}
|
||||
devAccess={{
|
||||
ready: devLoginReady,
|
||||
selected,
|
||||
pending,
|
||||
options: ROLE_OPTIONS,
|
||||
onSelect: setSelected,
|
||||
onEnter: (role) => {
|
||||
void enterDev(role);
|
||||
},
|
||||
}}
|
||||
error={{ message: loginError, reason: loginErrorReason }}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
const LOGIN_CSS = `
|
||||
.lg-root{
|
||||
min-height:100dvh;
|
||||
position:relative;
|
||||
display:grid;
|
||||
grid-template-columns:minmax(0,1fr) minmax(360px,500px);
|
||||
background:
|
||||
linear-gradient(90deg,rgba(7,16,14,.84) 0%,rgba(10,21,19,.72) 48%,rgba(10,21,19,.55) 74%,rgba(10,21,19,.62) 100%),
|
||||
var(--asset-login-room) center / cover no-repeat;
|
||||
color:#edf4f2;
|
||||
overflow-x:hidden;
|
||||
overflow-y:auto;
|
||||
}
|
||||
.lg-root::before{
|
||||
content:"";
|
||||
position:absolute;
|
||||
inset:0;
|
||||
background:linear-gradient(180deg,rgba(3,9,8,.18),rgba(3,9,8,.42));
|
||||
pointer-events:none;
|
||||
}
|
||||
.lg-brand,
|
||||
.lg-enter{
|
||||
position:relative;
|
||||
z-index:1;
|
||||
}
|
||||
.lg-brand{
|
||||
min-width:0;
|
||||
width:100%;
|
||||
position:relative;
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
justify-content:space-between;
|
||||
gap:var(--sp-7);
|
||||
padding:var(--sp-7);
|
||||
background:transparent;
|
||||
color:#edf4f2;
|
||||
overflow:hidden;
|
||||
}
|
||||
.lg-brand::after{
|
||||
display:none;
|
||||
}
|
||||
.lg-wordmark{
|
||||
position:relative;
|
||||
z-index:1;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
gap:10px;
|
||||
font-size:19px;
|
||||
font-weight:700;
|
||||
letter-spacing:0;
|
||||
color:#edf4f2;
|
||||
}
|
||||
.lg-mark{display:grid;place-items:center;color:var(--accent-bright);}
|
||||
.lg-copy{max-width:620px;}
|
||||
.lg-copy,
|
||||
.lg-policy{
|
||||
position:relative;
|
||||
z-index:1;
|
||||
}
|
||||
.lg-kicker{
|
||||
display:inline-flex;
|
||||
align-items:center;
|
||||
gap:8px;
|
||||
font-family:var(--font-num);
|
||||
font-size:12px;
|
||||
font-weight:700;
|
||||
letter-spacing:0;
|
||||
text-transform:uppercase;
|
||||
color:var(--accent-bright);
|
||||
}
|
||||
.lg-kicker .d{width:6px;height:6px;border-radius:50%;background:currentColor;}
|
||||
.lg-highlight{
|
||||
display:inline-block;
|
||||
position:relative;
|
||||
z-index:0;
|
||||
padding:0 4px;
|
||||
background:linear-gradient(90deg,#59b5a6,#8ee4d6);
|
||||
-webkit-background-clip:text;
|
||||
background-clip:text;
|
||||
-webkit-text-fill-color:transparent;
|
||||
color:transparent;
|
||||
}
|
||||
.lg-copy h1{
|
||||
margin:var(--sp-4) 0 0;
|
||||
max-width:640px;
|
||||
font-size:56px;
|
||||
line-height:1.12;
|
||||
letter-spacing:0;
|
||||
font-weight:760;
|
||||
}
|
||||
.lg-copy p{
|
||||
margin:var(--sp-5) 0 0;
|
||||
max-width:560px;
|
||||
color:rgba(237,244,242,.72);
|
||||
font-size:17px;
|
||||
line-height:1.75;
|
||||
}
|
||||
.lg-policy{
|
||||
display:flex;
|
||||
align-items:center;
|
||||
flex-wrap:wrap;
|
||||
gap:10px;
|
||||
width:max-content;
|
||||
max-width:min(430px,100%);
|
||||
padding:14px 16px;
|
||||
border:1px solid rgba(255,255,255,.14);
|
||||
border-radius:var(--radius-lg);
|
||||
background:rgba(237,244,242,.08);
|
||||
backdrop-filter:blur(18px) saturate(1.08);
|
||||
-webkit-backdrop-filter:blur(18px) saturate(1.08);
|
||||
color:rgba(237,244,242,.66);
|
||||
font-size:13px;
|
||||
}
|
||||
.lg-policy span,.lg-policy b{
|
||||
display:inline-flex;
|
||||
align-items:center;
|
||||
gap:7px;
|
||||
}
|
||||
.lg-policy b{
|
||||
color:#edf4f2;
|
||||
background:rgba(255,255,255,.08);
|
||||
border:1px solid rgba(255,255,255,.12);
|
||||
border-radius:999px;
|
||||
padding:5px 10px;
|
||||
font-weight:650;
|
||||
}
|
||||
.lg-enter{
|
||||
min-width:0;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
padding:var(--sp-6);
|
||||
background:transparent;
|
||||
}
|
||||
.lg-panel{
|
||||
width:100%;
|
||||
max-width:430px;
|
||||
background:rgba(11,25,22,.64);
|
||||
border:1px solid rgba(255,255,255,.16);
|
||||
border-radius:var(--radius-lg);
|
||||
box-shadow:0 24px 70px rgba(0,0,0,.34), inset 0 1px 0 rgba(255,255,255,.08);
|
||||
backdrop-filter:blur(24px) saturate(1.14);
|
||||
-webkit-backdrop-filter:blur(24px) saturate(1.14);
|
||||
padding:var(--sp-6);
|
||||
color:#edf4f2;
|
||||
}
|
||||
.lg-panel h2{
|
||||
margin:var(--sp-3) 0 0;
|
||||
font-size:28px;
|
||||
line-height:1.25;
|
||||
letter-spacing:0;
|
||||
color:#f7fbf9;
|
||||
}
|
||||
.lg-lead{
|
||||
margin:10px 0 0;
|
||||
color:rgba(237,244,242,.76);
|
||||
font-size:14px;
|
||||
line-height:1.65;
|
||||
}
|
||||
.lg-actions{display:flex;flex-direction:column;gap:var(--sp-3);margin-top:var(--sp-6);}
|
||||
.lg-obtn{
|
||||
width:100%;
|
||||
min-height:58px;
|
||||
display:grid;
|
||||
grid-template-columns:36px minmax(0,1fr) 18px;
|
||||
align-items:center;
|
||||
gap:13px;
|
||||
border-radius:var(--radius);
|
||||
padding:11px 14px;
|
||||
font-family:var(--font-sans);
|
||||
font-size:15px;
|
||||
font-weight:650;
|
||||
text-align:left;
|
||||
cursor:pointer;
|
||||
}
|
||||
.lg-obtn.primary{
|
||||
background:rgba(89,181,166,.2);
|
||||
border:1px solid rgba(114,211,197,.44);
|
||||
color:#f7fbf9;
|
||||
}
|
||||
.lg-obtn.primary:hover{background:rgba(89,181,166,.28);border-color:rgba(114,211,197,.62);}
|
||||
.lg-obtn.secondary{
|
||||
background:rgba(255,255,255,.08);
|
||||
border:1px solid rgba(255,255,255,.13);
|
||||
color:#edf4f2;
|
||||
}
|
||||
.lg-obtn.secondary:hover{border-color:rgba(114,211,197,.42);background:rgba(255,255,255,.11);}
|
||||
.lg-obtn:disabled{
|
||||
cursor:not-allowed;
|
||||
opacity:1;
|
||||
background:rgba(255,255,255,.07);
|
||||
border-color:rgba(255,255,255,.1);
|
||||
color:rgba(237,244,242,.58);
|
||||
}
|
||||
.lg-obtn:disabled:hover{
|
||||
background:rgba(255,255,255,.07);
|
||||
border-color:rgba(255,255,255,.1);
|
||||
}
|
||||
.lg-obtn:disabled .ic{
|
||||
background:rgba(255,255,255,.08);
|
||||
color:rgba(237,244,242,.58);
|
||||
}
|
||||
.lg-obtn:disabled .sub{
|
||||
color:rgba(237,244,242,.45);
|
||||
}
|
||||
.lg-obtn .ic{
|
||||
width:36px;
|
||||
height:36px;
|
||||
display:grid;
|
||||
place-items:center;
|
||||
border-radius:8px;
|
||||
background:rgba(255,255,255,.16);
|
||||
}
|
||||
.lg-obtn.secondary .ic{background:rgba(7,17,15,.34);}
|
||||
.lg-obtn .txt{min-width:0;display:flex;flex-direction:column;gap:1px;}
|
||||
.lg-obtn .sub{font-size:12px;font-weight:550;color:rgba(237,244,242,.58);}
|
||||
.lg-obtn.primary .sub{color:rgba(251,250,248,.74);}
|
||||
.lg-config{
|
||||
display:flex;
|
||||
align-items:flex-start;
|
||||
gap:10px;
|
||||
margin-top:var(--sp-3);
|
||||
padding:10px 12px;
|
||||
border-radius:var(--radius);
|
||||
background:rgba(154,94,20,.28);
|
||||
border:1px solid rgba(236,180,91,.16);
|
||||
color:#f2b75f;
|
||||
font-size:12.5px;
|
||||
line-height:1.5;
|
||||
}
|
||||
.lg-dev{
|
||||
margin-top:var(--sp-6);
|
||||
padding-top:var(--sp-5);
|
||||
border-top:1px solid rgba(255,255,255,.1);
|
||||
}
|
||||
.lg-devhead{
|
||||
display:flex;
|
||||
align-items:baseline;
|
||||
justify-content:space-between;
|
||||
gap:var(--sp-3);
|
||||
color:#edf4f2;
|
||||
font-size:13px;
|
||||
font-weight:700;
|
||||
}
|
||||
.lg-devhead small{color:rgba(237,244,242,.58);font-weight:600;}
|
||||
.lg-rolepick{display:grid;grid-template-columns:1fr;gap:var(--sp-2);margin-top:var(--sp-3);}
|
||||
.lg-roleopt{
|
||||
min-height:50px;
|
||||
display:grid;
|
||||
grid-template-columns:10px minmax(0,1fr) 16px;
|
||||
align-items:center;
|
||||
gap:10px;
|
||||
padding:9px 11px;
|
||||
border:1px solid rgba(255,255,255,.11);
|
||||
border-radius:var(--radius);
|
||||
background:rgba(255,255,255,.045);
|
||||
color:#edf4f2;
|
||||
text-align:left;
|
||||
cursor:pointer;
|
||||
}
|
||||
.lg-roleopt.is-sel{border-color:rgba(114,211,197,.58);background:rgba(89,181,166,.15);}
|
||||
.lg-roleopt .d{width:8px;height:8px;border-radius:50%;}
|
||||
.lg-roleopt .d.learner{background:var(--accent-bright);}
|
||||
.lg-roleopt .d.teacher{background:#5478c4;}
|
||||
.lg-roleopt .d.admin{background:#7d818e;}
|
||||
.lg-roleopt b{display:block;font-size:13px;}
|
||||
.lg-roleopt small{display:block;margin-top:1px;color:rgba(237,244,242,.58);font-size:12px;}
|
||||
.lg-devbtn{
|
||||
width:100%;
|
||||
min-height:46px;
|
||||
margin-top:var(--sp-3);
|
||||
border:1px solid rgba(114,211,197,.58);
|
||||
border-radius:var(--radius);
|
||||
background:rgba(89,181,166,.15);
|
||||
color:#8fe7d9;
|
||||
font-family:var(--font-sans);
|
||||
font-size:14px;
|
||||
font-weight:750;
|
||||
cursor:pointer;
|
||||
}
|
||||
.lg-devbtn:disabled{opacity:.62;cursor:wait;}
|
||||
.lg-error{
|
||||
margin:var(--sp-3) 0 0;
|
||||
color:var(--crit-text);
|
||||
background:rgba(122,38,38,.28);
|
||||
border:1px solid rgba(255,145,145,.18);
|
||||
border-radius:var(--radius);
|
||||
padding:10px 12px;
|
||||
font-size:13px;
|
||||
line-height:1.5;
|
||||
}
|
||||
.lg-error small{
|
||||
display:block;
|
||||
margin-top:4px;
|
||||
color:rgba(237,244,242,.56);
|
||||
font-family:var(--font-num);
|
||||
font-size:11.5px;
|
||||
overflow-wrap:anywhere;
|
||||
}
|
||||
.lg-note{
|
||||
margin:var(--sp-5) 0 0;
|
||||
color:rgba(237,244,242,.52);
|
||||
font-size:12.5px;
|
||||
line-height:1.6;
|
||||
}
|
||||
@media (max-width:880px){
|
||||
.lg-root{grid-template-columns:1fr;}
|
||||
.lg-root::before{display:none;}
|
||||
.lg-brand{padding:var(--sp-6) var(--sp-5);gap:var(--sp-6);}
|
||||
.lg-brand::after{display:none;}
|
||||
.lg-copy h1{font-size:36px;}
|
||||
.lg-enter{padding:var(--sp-5);}
|
||||
.lg-panel{max-width:560px;}
|
||||
}
|
||||
@media (max-width:480px){
|
||||
.lg-brand{padding:var(--sp-5);}
|
||||
.lg-copy h1{font-size:30px;}
|
||||
.lg-copy p{font-size:15px;}
|
||||
.lg-enter{padding:var(--sp-4);}
|
||||
.lg-panel{padding:var(--sp-5);}
|
||||
}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { useNavigate } from "react-router-dom";
|
|||
import { Button } from "../components/ui";
|
||||
import { roleHomePath, useAuth } from "../lib/auth";
|
||||
import { apiUrl, userApi, type LegalDocumentsResponse, type UserProfileResponse } from "../lib/api";
|
||||
import "./onboarding.css";
|
||||
|
||||
interface OnboardingForm {
|
||||
legal_name: string;
|
||||
|
|
@ -146,7 +147,6 @@ export default function Onboarding() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<style>{ONBOARDING_CSS}</style>
|
||||
<main className="ob-page">
|
||||
<section className="ob-shell" aria-label="가입 정보 입력">
|
||||
<header className="ob-head">
|
||||
|
|
@ -337,259 +337,3 @@ export default function Onboarding() {
|
|||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const ONBOARDING_CSS = `
|
||||
.ob-page{
|
||||
min-height:100dvh;
|
||||
width:100%;
|
||||
background:var(--bg-app);
|
||||
color:var(--text-body);
|
||||
padding:clamp(20px,5vw,56px);
|
||||
}
|
||||
.ob-shell{
|
||||
width:100%;
|
||||
max-width:900px;
|
||||
margin:0 auto;
|
||||
display:grid;
|
||||
gap:var(--sp-6);
|
||||
}
|
||||
.ob-head{
|
||||
display:grid;
|
||||
gap:8px;
|
||||
}
|
||||
.ob-head p{
|
||||
margin:0;
|
||||
color:var(--accent-deep);
|
||||
font-size:12px;
|
||||
font-weight:800;
|
||||
}
|
||||
.ob-head h1{
|
||||
margin:0;
|
||||
color:var(--text-strong);
|
||||
font-size:clamp(28px,4vw,44px);
|
||||
line-height:1.18;
|
||||
letter-spacing:0;
|
||||
}
|
||||
.ob-form{
|
||||
min-width:0;
|
||||
display:grid;
|
||||
gap:var(--sp-6);
|
||||
}
|
||||
.ob-section{
|
||||
min-width:0;
|
||||
display:grid;
|
||||
gap:var(--sp-4);
|
||||
padding-bottom:var(--sp-5);
|
||||
border-bottom:1px solid var(--border-subtle);
|
||||
}
|
||||
.ob-section__head h2{
|
||||
margin:0;
|
||||
color:var(--text-strong);
|
||||
font-size:18px;
|
||||
line-height:1.35;
|
||||
letter-spacing:0;
|
||||
}
|
||||
.ob-avatar{
|
||||
display:grid;
|
||||
grid-template-columns:auto minmax(0,1fr);
|
||||
gap:var(--sp-3);
|
||||
align-items:center;
|
||||
}
|
||||
.ob-avatar__preview{
|
||||
width:72px;
|
||||
height:72px;
|
||||
border-radius:50%;
|
||||
display:grid;
|
||||
place-items:center;
|
||||
overflow:hidden;
|
||||
background:var(--accent-tint);
|
||||
color:var(--accent-deep);
|
||||
font-size:28px;
|
||||
font-weight:800;
|
||||
border:1px solid var(--border-subtle);
|
||||
}
|
||||
.ob-avatar__preview img{
|
||||
width:100%;
|
||||
height:100%;
|
||||
display:block;
|
||||
object-fit:cover;
|
||||
}
|
||||
.ob-avatar__body{
|
||||
min-width:0;
|
||||
display:grid;
|
||||
gap:7px;
|
||||
}
|
||||
.ob-avatar__body span{
|
||||
color:var(--text-strong);
|
||||
font-size:13px;
|
||||
font-weight:780;
|
||||
}
|
||||
.ob-avatar__body p{
|
||||
margin:0;
|
||||
color:var(--text-muted);
|
||||
font-size:12.5px;
|
||||
line-height:1.45;
|
||||
}
|
||||
.ob-avatar__button{
|
||||
position:relative;
|
||||
width:max-content;
|
||||
min-height:34px;
|
||||
display:inline-flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
padding:0 12px;
|
||||
border:1px solid var(--border-subtle);
|
||||
border-radius:var(--radius-sm);
|
||||
background:var(--bg-surface);
|
||||
color:var(--text-strong);
|
||||
font-size:12px;
|
||||
font-weight:760;
|
||||
cursor:pointer;
|
||||
}
|
||||
.ob-avatar__button input{
|
||||
position:absolute;
|
||||
inline-size:1px;
|
||||
block-size:1px;
|
||||
opacity:0;
|
||||
pointer-events:none;
|
||||
}
|
||||
.ob-field--wide{
|
||||
grid-column:1 / -1;
|
||||
}
|
||||
.ob-fields{
|
||||
display:grid;
|
||||
grid-template-columns:repeat(2,minmax(0,1fr));
|
||||
gap:var(--sp-3);
|
||||
}
|
||||
.ob-form label{
|
||||
min-width:0;
|
||||
display:grid;
|
||||
gap:7px;
|
||||
}
|
||||
.ob-form label span{
|
||||
color:var(--text-muted);
|
||||
font-size:12px;
|
||||
font-weight:730;
|
||||
}
|
||||
.ob-form input[type="text"],
|
||||
.ob-form input:not([type]){
|
||||
min-width:0;
|
||||
}
|
||||
.ob-form input,
|
||||
.ob-form textarea{
|
||||
width:100%;
|
||||
border:1px solid var(--border-subtle);
|
||||
border-radius:var(--radius-sm);
|
||||
background:var(--bg-surface-2);
|
||||
color:var(--text-strong);
|
||||
padding:0 12px;
|
||||
font:500 var(--fs-sm)/1.2 var(--font-sans);
|
||||
}
|
||||
.ob-form input{
|
||||
min-height:42px;
|
||||
}
|
||||
.ob-form textarea{
|
||||
min-height:92px;
|
||||
padding:11px 12px;
|
||||
resize:vertical;
|
||||
line-height:1.45;
|
||||
}
|
||||
.ob-form input:focus,
|
||||
.ob-form textarea:focus{
|
||||
outline:2px solid color-mix(in srgb,var(--accent) 24%,transparent);
|
||||
border-color:var(--accent);
|
||||
}
|
||||
.ob-checks{
|
||||
display:grid;
|
||||
gap:10px;
|
||||
padding:14px;
|
||||
border:1px solid var(--border-subtle);
|
||||
border-radius:var(--radius);
|
||||
background:var(--bg-surface-2);
|
||||
}
|
||||
.ob-checks label{
|
||||
grid-template-columns:auto minmax(0,1fr);
|
||||
align-items:center;
|
||||
gap:10px;
|
||||
}
|
||||
.ob-checks input{
|
||||
width:18px;
|
||||
height:18px;
|
||||
min-height:18px;
|
||||
padding:0;
|
||||
accent-color:var(--accent);
|
||||
}
|
||||
.ob-legal{
|
||||
min-width:0;
|
||||
display:grid;
|
||||
gap:8px;
|
||||
}
|
||||
.ob-legal details{
|
||||
min-width:0;
|
||||
border:1px solid var(--border-subtle);
|
||||
border-radius:var(--radius-sm);
|
||||
background:var(--bg-surface-2);
|
||||
}
|
||||
.ob-legal summary{
|
||||
min-width:0;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:space-between;
|
||||
gap:10px;
|
||||
min-height:42px;
|
||||
padding:0 12px;
|
||||
cursor:pointer;
|
||||
}
|
||||
.ob-legal summary span{
|
||||
min-width:0;
|
||||
color:var(--text-strong);
|
||||
font-size:13px;
|
||||
font-weight:760;
|
||||
overflow:hidden;
|
||||
text-overflow:ellipsis;
|
||||
white-space:nowrap;
|
||||
}
|
||||
.ob-legal summary b{
|
||||
flex:none;
|
||||
color:var(--text-muted);
|
||||
font-size:11px;
|
||||
font-weight:700;
|
||||
}
|
||||
.ob-note,
|
||||
.ob-error{
|
||||
margin:0;
|
||||
color:var(--text-muted);
|
||||
font-size:12.5px;
|
||||
line-height:1.5;
|
||||
}
|
||||
.ob-doc-text{
|
||||
max-height:260px;
|
||||
overflow:auto;
|
||||
white-space:pre-wrap;
|
||||
color:var(--text-body);
|
||||
font-size:12.5px;
|
||||
line-height:1.55;
|
||||
padding:0 12px 12px;
|
||||
}
|
||||
.ob-error{
|
||||
color:var(--crit-text);
|
||||
}
|
||||
.ob-actions{
|
||||
display:flex;
|
||||
justify-content:flex-start;
|
||||
}
|
||||
@media (max-width:620px){
|
||||
.ob-page{
|
||||
padding:14px;
|
||||
}
|
||||
.ob-fields{
|
||||
grid-template-columns:1fr;
|
||||
}
|
||||
.ob-avatar{
|
||||
grid-template-columns:1fr;
|
||||
}
|
||||
.ob-actions .vg-btn{
|
||||
width:100%;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { useState } from "react";
|
|||
import { useNavigate } from "react-router-dom";
|
||||
import { Button, Icon } from "../components/ui";
|
||||
import { roleHomePath, useAuth } from "../lib/auth";
|
||||
import "./pending-approval.css";
|
||||
|
||||
export default function PendingApproval() {
|
||||
const navigate = useNavigate();
|
||||
|
|
@ -33,7 +34,6 @@ export default function PendingApproval() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<style>{PENDING_APPROVAL_CSS}</style>
|
||||
<main className="pa-page" aria-label="계정 승인 대기">
|
||||
<section className="pa-panel">
|
||||
<div className="pa-mark" aria-hidden="true">
|
||||
|
|
@ -77,100 +77,3 @@ export default function PendingApproval() {
|
|||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const PENDING_APPROVAL_CSS = `
|
||||
.pa-page{
|
||||
min-height:100dvh;
|
||||
display:grid;
|
||||
place-items:center;
|
||||
padding:clamp(18px,5vw,56px);
|
||||
background:var(--bg-app);
|
||||
color:var(--text-body);
|
||||
}
|
||||
.pa-panel{
|
||||
width:min(100%,620px);
|
||||
display:grid;
|
||||
gap:var(--sp-4);
|
||||
padding:clamp(24px,5vw,44px);
|
||||
border:1px solid var(--border-subtle);
|
||||
border-radius:var(--radius);
|
||||
background:color-mix(in srgb,var(--bg-surface) 92%,white 8%);
|
||||
box-shadow:0 18px 50px rgba(28,43,40,.10);
|
||||
}
|
||||
.pa-mark{
|
||||
width:62px;
|
||||
height:62px;
|
||||
display:grid;
|
||||
place-items:center;
|
||||
border-radius:var(--radius);
|
||||
background:var(--accent-tint);
|
||||
color:var(--accent-deep);
|
||||
border:1px solid color-mix(in srgb,var(--accent) 20%,transparent);
|
||||
}
|
||||
.pa-kicker{
|
||||
margin:0;
|
||||
color:var(--accent-deep);
|
||||
font-size:12px;
|
||||
font-weight:820;
|
||||
}
|
||||
.pa-panel h1{
|
||||
margin:0;
|
||||
color:var(--text-strong);
|
||||
font-size:clamp(30px,5vw,46px);
|
||||
line-height:1.17;
|
||||
letter-spacing:0;
|
||||
}
|
||||
.pa-copy{
|
||||
margin:0;
|
||||
color:var(--text-muted);
|
||||
font-size:15px;
|
||||
line-height:1.7;
|
||||
}
|
||||
.pa-copy b{
|
||||
color:var(--text-strong);
|
||||
font-weight:760;
|
||||
}
|
||||
.pa-status{
|
||||
min-width:0;
|
||||
display:grid;
|
||||
grid-template-columns:auto minmax(0,1fr);
|
||||
gap:12px;
|
||||
align-items:center;
|
||||
padding:14px;
|
||||
border:1px solid var(--border-subtle);
|
||||
border-radius:var(--radius);
|
||||
background:var(--bg-surface-2);
|
||||
}
|
||||
.pa-status > span{
|
||||
width:10px;
|
||||
height:10px;
|
||||
border-radius:50%;
|
||||
background:#d89a2b;
|
||||
box-shadow:0 0 0 5px rgba(216,154,43,.14);
|
||||
}
|
||||
.pa-status b{
|
||||
display:block;
|
||||
color:var(--text-strong);
|
||||
font-size:14px;
|
||||
}
|
||||
.pa-status small{
|
||||
display:block;
|
||||
margin-top:3px;
|
||||
color:var(--text-muted);
|
||||
font-size:12.5px;
|
||||
line-height:1.45;
|
||||
}
|
||||
.pa-actions{
|
||||
display:flex;
|
||||
flex-wrap:wrap;
|
||||
gap:10px;
|
||||
}
|
||||
@media (max-width:560px){
|
||||
.pa-panel{
|
||||
border-radius:var(--radius);
|
||||
}
|
||||
.pa-actions .vg-btn{
|
||||
width:100%;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -14,6 +14,7 @@ import {
|
|||
type TeacherSafetyAlert,
|
||||
type TeacherDashboardResponse,
|
||||
} from "../lib/api";
|
||||
import "./professor.css";
|
||||
|
||||
type LoadState = "loading" | "ready" | "error";
|
||||
|
||||
|
|
@ -84,7 +85,6 @@ function EmptyState({ title, desc }: { title: string; desc: string }) {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Professor() {
|
||||
const navigate = useNavigate();
|
||||
const [dashboard, setDashboard] = useState<TeacherDashboardResponse | null>(null);
|
||||
|
|
@ -212,7 +212,6 @@ export default function Professor() {
|
|||
|
||||
return (
|
||||
<AppShell navRole="teacher" wide>
|
||||
<style>{PF_CSS}</style>
|
||||
<main className="pf-root">
|
||||
<header className="pf-head">
|
||||
<div>
|
||||
|
|
@ -719,920 +718,3 @@ function SafetyAlertRow({ alert }: { alert: TeacherSafetyAlert }) {
|
|||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
const PF_CSS = `
|
||||
.pf-root{
|
||||
max-width:1280px;
|
||||
margin:0 auto;
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
gap:16px;
|
||||
}
|
||||
.pf-head{
|
||||
display:flex;
|
||||
align-items:flex-end;
|
||||
justify-content:space-between;
|
||||
gap:var(--sp-4);
|
||||
flex-wrap:wrap;
|
||||
}
|
||||
.pf-head__actions{
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:flex-end;
|
||||
gap:8px;
|
||||
flex-wrap:wrap;
|
||||
}
|
||||
.pf-head h1{
|
||||
margin:6px 0 0;
|
||||
color:var(--text-strong);
|
||||
font-size:var(--fs-h2);
|
||||
line-height:1.28;
|
||||
letter-spacing:0;
|
||||
}
|
||||
.pf-head p{
|
||||
margin:6px 0 0;
|
||||
max-width:760px;
|
||||
color:var(--text-body);
|
||||
font-size:var(--fs-xs);
|
||||
line-height:1.55;
|
||||
}
|
||||
.pf-signal-strip{
|
||||
order:1;
|
||||
display:grid;
|
||||
grid-template-columns:1fr;
|
||||
gap:0;
|
||||
min-width:0;
|
||||
}
|
||||
.pf-triage{
|
||||
display:grid;
|
||||
grid-template-columns:auto minmax(0,1fr) auto;
|
||||
align-items:center;
|
||||
gap:12px;
|
||||
min-width:0;
|
||||
padding:14px 16px;
|
||||
border:1px solid var(--hair);
|
||||
border-radius:var(--radius);
|
||||
background:var(--bg-surface);
|
||||
box-shadow:var(--shadow-sm);
|
||||
}
|
||||
.pf-triage.is-active{
|
||||
background:color-mix(in srgb,var(--accent-tint) 42%,var(--bg-surface));
|
||||
}
|
||||
.pf-triage__copy{
|
||||
min-width:0;
|
||||
}
|
||||
.pf-triage__copy .vg-kicker{
|
||||
margin-bottom:4px;
|
||||
}
|
||||
.pf-triage__copy b{
|
||||
display:block;
|
||||
color:var(--text-strong);
|
||||
font-size:var(--fs-body);
|
||||
line-height:1.35;
|
||||
}
|
||||
.pf-triage__copy span,
|
||||
.pf-triage__meta small{
|
||||
color:var(--text-muted);
|
||||
font-size:var(--fs-xs);
|
||||
}
|
||||
.pf-triage__meta{
|
||||
display:grid;
|
||||
grid-template-columns:repeat(2,minmax(0,1fr));
|
||||
gap:0;
|
||||
min-width:180px;
|
||||
border:1px solid var(--border-subtle);
|
||||
border-radius:var(--radius);
|
||||
background:color-mix(in srgb,var(--bg-surface) 86%,transparent);
|
||||
overflow:hidden;
|
||||
}
|
||||
.pf-triage__meta span{
|
||||
display:grid;
|
||||
align-content:center;
|
||||
gap:2px;
|
||||
padding:9px 12px;
|
||||
}
|
||||
.pf-triage__meta span + span{
|
||||
border-left:1px solid var(--hair);
|
||||
}
|
||||
.pf-triage__meta b{
|
||||
color:var(--text-strong);
|
||||
font-family:var(--font-num);
|
||||
font-size:17px;
|
||||
line-height:1;
|
||||
}
|
||||
.pf-error{
|
||||
display:flex;
|
||||
align-items:center;
|
||||
gap:10px;
|
||||
padding:12px 14px;
|
||||
border-radius:var(--radius);
|
||||
background:var(--crit-tint);
|
||||
color:var(--crit-text);
|
||||
font-size:var(--fs-sm);
|
||||
}
|
||||
.pf-kpis{
|
||||
display:grid;
|
||||
grid-template-columns:repeat(6,minmax(0,1fr));
|
||||
border:1px solid var(--hair);
|
||||
border-radius:var(--radius);
|
||||
overflow:hidden;
|
||||
background:var(--bg-surface);
|
||||
box-shadow:var(--shadow-sm);
|
||||
}
|
||||
.pf-kpi{
|
||||
position:relative;
|
||||
min-width:0;
|
||||
padding:14px 16px;
|
||||
display:grid;
|
||||
grid-template-columns:minmax(0,1fr) auto;
|
||||
gap:4px 10px;
|
||||
}
|
||||
.pf-kpi:nth-child(n+2){border-left:1px solid var(--hair);}
|
||||
.pf-kpi:nth-child(n+4){border-top:0;}
|
||||
.pf-kpi--primary,
|
||||
.pf-kpi--warn{
|
||||
background:color-mix(in srgb,var(--accent-tint) 32%,var(--bg-surface));
|
||||
}
|
||||
.pf-kpi--warn{
|
||||
background:color-mix(in srgb,var(--warn-tint) 36%,var(--bg-surface));
|
||||
}
|
||||
.pf-kpi--primary .pf-kpi__ic{
|
||||
color:var(--text-on-accent);
|
||||
background:var(--accent);
|
||||
}
|
||||
.pf-kpi--warn .pf-kpi__ic{
|
||||
color:var(--warn-text);
|
||||
background:var(--warn-tint);
|
||||
}
|
||||
.pf-kpi__ic{
|
||||
grid-column:2;
|
||||
grid-row:1 / span 3;
|
||||
width:30px;
|
||||
height:30px;
|
||||
display:grid;
|
||||
place-items:center;
|
||||
border-radius:var(--radius);
|
||||
color:var(--accent);
|
||||
background:var(--accent-tint);
|
||||
}
|
||||
.pf-kpi__lab{
|
||||
display:block;
|
||||
color:var(--text-muted);
|
||||
font-size:var(--fs-xs);
|
||||
font-weight:700;
|
||||
}
|
||||
.pf-kpi b{
|
||||
display:block;
|
||||
color:var(--text-strong);
|
||||
font-family:var(--font-num);
|
||||
font-size:24px;
|
||||
line-height:1;
|
||||
}
|
||||
.pf-kpi small{
|
||||
color:var(--text-muted);
|
||||
font-size:12px;
|
||||
line-height:1.35;
|
||||
}
|
||||
.pf-workspace{
|
||||
order:2;
|
||||
display:grid;
|
||||
grid-template-columns:minmax(320px,390px) minmax(0,1fr);
|
||||
align-items:start;
|
||||
gap:14px;
|
||||
min-width:0;
|
||||
}
|
||||
.pf-queue-stack{
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
gap:14px;
|
||||
min-width:0;
|
||||
}
|
||||
.pf-section{
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
gap:10px;
|
||||
min-width:0;
|
||||
}
|
||||
.pf-section__head{
|
||||
display:flex;
|
||||
align-items:flex-end;
|
||||
justify-content:space-between;
|
||||
gap:12px;
|
||||
}
|
||||
.pf-section__head h2{
|
||||
margin:4px 0 0;
|
||||
color:var(--text-strong);
|
||||
font-size:var(--fs-body);
|
||||
font-weight:700;
|
||||
line-height:1.3;
|
||||
letter-spacing:0;
|
||||
}
|
||||
.pf-panel{
|
||||
padding:0;
|
||||
overflow:hidden;
|
||||
}
|
||||
.pf-studio-card{
|
||||
display:grid;
|
||||
grid-template-columns:minmax(0,1fr) auto;
|
||||
gap:14px;
|
||||
align-items:center;
|
||||
padding:14px;
|
||||
background:linear-gradient(180deg,var(--bg-surface),color-mix(in srgb,var(--accent-tint) 18%,var(--bg-surface)));
|
||||
}
|
||||
.pf-studio-card__copy{
|
||||
min-width:0;
|
||||
display:grid;
|
||||
gap:7px;
|
||||
}
|
||||
.pf-studio-card__copy b{
|
||||
color:var(--text-strong);
|
||||
font-size:var(--fs-body);
|
||||
}
|
||||
.pf-studio-card__copy p{
|
||||
margin:0;
|
||||
color:var(--text-muted);
|
||||
font-size:var(--fs-xs);
|
||||
line-height:1.55;
|
||||
}
|
||||
.pf-studio-card__meta{
|
||||
display:flex;
|
||||
flex-wrap:wrap;
|
||||
gap:6px;
|
||||
}
|
||||
.pf-studio-card__meta span{
|
||||
padding:4px 7px;
|
||||
border:1px solid var(--hair);
|
||||
border-radius:var(--radius-sm);
|
||||
color:var(--text-body);
|
||||
background:var(--bg-surface-2);
|
||||
font-size:11px;
|
||||
line-height:1.2;
|
||||
}
|
||||
.pf-studio-card__actions{
|
||||
display:flex;
|
||||
justify-content:flex-end;
|
||||
}
|
||||
.pf-section--growth{
|
||||
order:3;
|
||||
min-width:0;
|
||||
}
|
||||
.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;
|
||||
display:grid;
|
||||
grid-template-columns:repeat(3,minmax(0,1fr));
|
||||
gap:12px;
|
||||
padding:12px;
|
||||
}
|
||||
.pf-growth-card{
|
||||
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;
|
||||
}
|
||||
.pf-growth-card__top{
|
||||
display:flex;
|
||||
align-items:flex-start;
|
||||
justify-content:space-between;
|
||||
gap:10px;
|
||||
min-width:0;
|
||||
}
|
||||
.pf-growth-card__id{
|
||||
min-width:0;
|
||||
display:grid;
|
||||
gap:3px;
|
||||
}
|
||||
.pf-growth-card__id b{
|
||||
color:var(--text-strong);
|
||||
font-size:var(--fs-sm);
|
||||
line-height:1.35;
|
||||
overflow:hidden;
|
||||
text-overflow:ellipsis;
|
||||
white-space:nowrap;
|
||||
}
|
||||
.pf-growth-card__id span,
|
||||
.pf-growth-card__metrics small,
|
||||
.pf-growth-point span,
|
||||
.pf-recent__review-state{
|
||||
color:var(--text-muted);
|
||||
font-size:12px;
|
||||
line-height:1.4;
|
||||
}
|
||||
.pf-recent__review-state{
|
||||
display:block;
|
||||
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;
|
||||
height:100%;
|
||||
display:grid;
|
||||
grid-template-rows:minmax(0,1fr) 14px;
|
||||
gap:4px;
|
||||
align-items:end;
|
||||
}
|
||||
.pf-growth-bar i{
|
||||
display:block;
|
||||
width:100%;
|
||||
min-height:6px;
|
||||
border-radius:6px 6px 3px 3px;
|
||||
background:linear-gradient(180deg,var(--accent),var(--accent-deep));
|
||||
}
|
||||
.pf-growth-bar.is-empty i{
|
||||
background:repeating-linear-gradient(135deg,var(--paper-2),var(--paper-2) 3px,var(--hair) 3px,var(--hair) 6px);
|
||||
}
|
||||
.pf-growth-bar small{
|
||||
color:var(--text-muted);
|
||||
font-family:var(--font-num);
|
||||
font-size:10px;
|
||||
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-empty{
|
||||
min-height:118px;
|
||||
display:grid;
|
||||
place-items:center;
|
||||
gap:6px;
|
||||
padding:var(--sp-5);
|
||||
text-align:center;
|
||||
}
|
||||
.pf-empty b{
|
||||
color:var(--text-strong);
|
||||
font-size:var(--fs-body);
|
||||
}
|
||||
.pf-empty span{
|
||||
color:var(--text-muted);
|
||||
font-size:var(--fs-xs);
|
||||
}
|
||||
.pf-list{
|
||||
max-height:min(320px,42vh);
|
||||
overflow:auto;
|
||||
scrollbar-gutter:stable;
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
}
|
||||
.pf-personas{
|
||||
max-height:min(320px,42vh);
|
||||
overflow:auto;
|
||||
scrollbar-gutter:stable;
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
}
|
||||
.pf-persona{
|
||||
display:grid;
|
||||
grid-template-columns:minmax(0,1fr);
|
||||
gap:8px;
|
||||
align-items:start;
|
||||
padding:12px;
|
||||
border-top:1px solid var(--paper-2);
|
||||
}
|
||||
.pf-persona:first-child{border-top:0;}
|
||||
.pf-persona__main{
|
||||
min-width:0;
|
||||
display:grid;
|
||||
grid-template-columns:40px minmax(0,1fr);
|
||||
gap:10px;
|
||||
align-items:center;
|
||||
}
|
||||
.pf-persona__code{
|
||||
width:40px;
|
||||
height:32px;
|
||||
display:grid;
|
||||
place-items:center;
|
||||
border-radius:var(--radius);
|
||||
color:var(--accent-deep);
|
||||
background:var(--accent-tint);
|
||||
font-family:var(--font-num);
|
||||
font-weight:800;
|
||||
font-size:12px;
|
||||
}
|
||||
.pf-persona__main b{
|
||||
display:block;
|
||||
color:var(--text-strong);
|
||||
font-size:var(--fs-sm);
|
||||
line-height:1.35;
|
||||
overflow:hidden;
|
||||
text-overflow:ellipsis;
|
||||
white-space:nowrap;
|
||||
}
|
||||
.pf-persona__main span,
|
||||
.pf-persona p,
|
||||
.pf-persona__meta span{
|
||||
color:var(--text-muted);
|
||||
font-size:var(--fs-xs);
|
||||
line-height:1.45;
|
||||
}
|
||||
.pf-persona p{
|
||||
margin:0;
|
||||
min-width:0;
|
||||
overflow:hidden;
|
||||
text-overflow:ellipsis;
|
||||
white-space:nowrap;
|
||||
}
|
||||
.pf-persona__meta{
|
||||
display:flex;
|
||||
align-items:center;
|
||||
gap:10px;
|
||||
justify-content:space-between;
|
||||
white-space:nowrap;
|
||||
}
|
||||
.pf-persona__actions{
|
||||
display:flex;
|
||||
justify-content:flex-end;
|
||||
gap:8px;
|
||||
min-width:0;
|
||||
}
|
||||
.pf-persona__actions .vg-btn{
|
||||
min-width:72px;
|
||||
padding-inline:10px;
|
||||
}
|
||||
.pf-alerts{
|
||||
max-height:min(300px,38vh);
|
||||
overflow:auto;
|
||||
scrollbar-gutter:stable;
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
}
|
||||
.pf-alert{
|
||||
display:grid;
|
||||
grid-template-columns:auto minmax(0,1fr) auto;
|
||||
gap:10px;
|
||||
align-items:center;
|
||||
padding:12px;
|
||||
border-top:1px solid var(--paper-2);
|
||||
background:color-mix(in srgb,var(--warn-tint) 34%,transparent);
|
||||
}
|
||||
.pf-alert:first-child{border-top:0;}
|
||||
.pf-alert__ic{
|
||||
width:30px;
|
||||
height:30px;
|
||||
display:grid;
|
||||
place-items:center;
|
||||
border-radius:var(--radius);
|
||||
color:var(--warn-text);
|
||||
background:var(--warn-tint);
|
||||
}
|
||||
.pf-alert__main{
|
||||
min-width:0;
|
||||
display:grid;
|
||||
gap:3px;
|
||||
}
|
||||
.pf-alert__main b{
|
||||
color:var(--text-strong);
|
||||
font-size:var(--fs-sm);
|
||||
}
|
||||
.pf-alert__main span,
|
||||
.pf-alert__main code,
|
||||
.pf-alert__resource span{
|
||||
color:var(--text-muted);
|
||||
font-size:12px;
|
||||
}
|
||||
.pf-alert__main code{
|
||||
overflow:hidden;
|
||||
text-overflow:ellipsis;
|
||||
white-space:nowrap;
|
||||
}
|
||||
.pf-alert__resource{
|
||||
display:grid;
|
||||
gap:2px;
|
||||
justify-items:end;
|
||||
min-width:86px;
|
||||
}
|
||||
.pf-alert__resource b{
|
||||
color:var(--warn-text);
|
||||
font-family:var(--font-num);
|
||||
font-size:18px;
|
||||
}
|
||||
.pf-session{
|
||||
width:100%;
|
||||
font:inherit;
|
||||
text-align:left;
|
||||
background:transparent;
|
||||
color:inherit;
|
||||
display:grid;
|
||||
grid-template-columns:8px minmax(0,1fr) auto;
|
||||
gap:8px 10px;
|
||||
align-items:start;
|
||||
padding:12px;
|
||||
border:0;
|
||||
border-top:1px solid var(--paper-2);
|
||||
cursor:default;
|
||||
}
|
||||
.pf-session:first-child{border-top:0;}
|
||||
.pf-session--action{
|
||||
cursor:pointer;
|
||||
}
|
||||
.pf-session--action:hover{
|
||||
background:var(--bg-surface-2);
|
||||
}
|
||||
.pf-session--action:focus-visible{
|
||||
outline:2px solid var(--accent);
|
||||
outline-offset:-2px;
|
||||
}
|
||||
.pf-session__dot{
|
||||
width:8px;
|
||||
height:8px;
|
||||
border-radius:50%;
|
||||
background:var(--accent);
|
||||
}
|
||||
.pf-session__main{
|
||||
min-width:0;
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
gap:3px;
|
||||
}
|
||||
.pf-session__main b{
|
||||
color:var(--text-strong);
|
||||
font-size:var(--fs-sm);
|
||||
}
|
||||
.pf-session__main span,.pf-session__main code{
|
||||
color:var(--text-muted);
|
||||
font-size:var(--fs-xs);
|
||||
overflow-wrap:anywhere;
|
||||
}
|
||||
.pf-session__main code,.pf-recent__learner code{
|
||||
font-family:var(--font-num);
|
||||
overflow:hidden;
|
||||
text-overflow:ellipsis;
|
||||
}
|
||||
.pf-session__meta{
|
||||
grid-column:2 / 4;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:space-between;
|
||||
gap:8px;
|
||||
color:var(--text-muted);
|
||||
font-family:var(--font-num);
|
||||
font-size:var(--fs-xs);
|
||||
white-space:normal;
|
||||
}
|
||||
.pf-session__open{
|
||||
grid-column:3;
|
||||
grid-row:1;
|
||||
display:inline-flex;
|
||||
align-items:center;
|
||||
gap:5px;
|
||||
align-self:center;
|
||||
justify-self:end;
|
||||
min-height:30px;
|
||||
padding:0 9px;
|
||||
border:1px solid var(--hair);
|
||||
border-radius:var(--radius-sm);
|
||||
color:var(--accent-deep);
|
||||
background:var(--accent-tint);
|
||||
font-size:var(--fs-xs);
|
||||
font-weight:760;
|
||||
white-space:nowrap;
|
||||
}
|
||||
.pf-session--action:hover .pf-session__open{
|
||||
border-color:var(--accent);
|
||||
}
|
||||
.pf-recent-list{
|
||||
max-height:min(620px,calc(100vh - 220px));
|
||||
overflow:auto;
|
||||
scrollbar-gutter:stable;
|
||||
min-width:0;
|
||||
}
|
||||
.pf-recent-head,
|
||||
.pf-recent-row{
|
||||
display:grid;
|
||||
grid-template-columns:
|
||||
minmax(128px,1.25fr)
|
||||
minmax(58px,.55fr)
|
||||
minmax(66px,.55fr)
|
||||
minmax(82px,.85fr)
|
||||
minmax(32px,.35fr)
|
||||
minmax(70px,.6fr)
|
||||
minmax(66px,.55fr)
|
||||
minmax(86px,.65fr);
|
||||
align-items:center;
|
||||
gap:8px;
|
||||
min-width:0;
|
||||
}
|
||||
.pf-recent-head{
|
||||
position:sticky;
|
||||
top:0;
|
||||
z-index:1;
|
||||
color:var(--text-muted);
|
||||
font-size:var(--fs-xs);
|
||||
font-weight:700;
|
||||
padding:9px 12px;
|
||||
border-bottom:1px solid var(--hair);
|
||||
background:var(--bg-surface);
|
||||
white-space:nowrap;
|
||||
}
|
||||
.pf-recent-row{
|
||||
width:100%;
|
||||
font:inherit;
|
||||
text-align:left;
|
||||
color:inherit;
|
||||
background:transparent;
|
||||
padding:10px 12px;
|
||||
border:0;
|
||||
border-top:1px solid var(--paper-2);
|
||||
}
|
||||
.pf-recent-row--action{
|
||||
cursor:pointer;
|
||||
}
|
||||
.pf-recent-row--action:hover{
|
||||
background:var(--bg-surface-2);
|
||||
}
|
||||
.pf-recent-row--action:focus-visible{
|
||||
outline:2px solid var(--accent);
|
||||
outline-offset:-2px;
|
||||
}
|
||||
.pf-recent-head + .pf-recent-row{
|
||||
border-top:0;
|
||||
}
|
||||
.pf-recent__learner,
|
||||
.pf-recent__cell{
|
||||
min-width:0;
|
||||
color:var(--text-body);
|
||||
font-size:var(--fs-sm);
|
||||
overflow-wrap:anywhere;
|
||||
}
|
||||
.pf-recent__cell::before{
|
||||
display:none;
|
||||
}
|
||||
.pf-recent__learner{
|
||||
min-width:0;
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
gap:3px;
|
||||
}
|
||||
.pf-recent__learner b{
|
||||
color:var(--text-strong);
|
||||
font-size:var(--fs-sm);
|
||||
line-height:1.35;
|
||||
overflow-wrap:anywhere;
|
||||
}
|
||||
.pf-recent__learner code{
|
||||
max-width:100%;
|
||||
color:var(--text-muted);
|
||||
font-size:11px;
|
||||
white-space:nowrap;
|
||||
overflow:hidden;
|
||||
text-overflow:ellipsis;
|
||||
}
|
||||
.pf-recent__cell .vg-badge{
|
||||
justify-self:start;
|
||||
}
|
||||
.pf-recent__cell--open{
|
||||
display:flex;
|
||||
justify-content:flex-end;
|
||||
}
|
||||
.pf-recent__open{
|
||||
display:inline-flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
gap:4px;
|
||||
min-height:30px;
|
||||
padding:0 7px;
|
||||
border:1px solid var(--hair);
|
||||
border-radius:var(--radius-sm);
|
||||
color:var(--accent-deep);
|
||||
background:var(--accent-tint);
|
||||
font-size:var(--fs-xs);
|
||||
font-weight:760;
|
||||
white-space:nowrap;
|
||||
}
|
||||
.pf-recent-row--action:hover .pf-recent__open{
|
||||
border-color:var(--accent);
|
||||
}
|
||||
@media (max-width:1100px){
|
||||
.pf-signal-strip,
|
||||
.pf-workspace{
|
||||
grid-template-columns:1fr;
|
||||
}
|
||||
.pf-triage{
|
||||
grid-template-columns:auto minmax(0,max-content) auto;
|
||||
justify-content:start;
|
||||
}
|
||||
.pf-kpis{
|
||||
grid-template-columns:repeat(4,minmax(0,1fr));
|
||||
}
|
||||
.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-list,
|
||||
.pf-personas{
|
||||
max-height:360px;
|
||||
}
|
||||
.pf-recent-list{
|
||||
max-height:460px;
|
||||
}
|
||||
}
|
||||
@media (max-width:860px){
|
||||
.pf-head__actions{
|
||||
width:100%;
|
||||
justify-content:flex-start;
|
||||
}
|
||||
.pf-triage{
|
||||
grid-template-columns:auto minmax(0,1fr);
|
||||
}
|
||||
.pf-triage__meta{
|
||||
grid-column:1 / -1;
|
||||
width:100%;
|
||||
}
|
||||
.pf-kpis{grid-template-columns:repeat(2,minmax(0,1fr));}
|
||||
.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-recent-list{
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
gap:10px;
|
||||
padding:10px;
|
||||
background:var(--bg-surface-2);
|
||||
overflow-x:hidden;
|
||||
}
|
||||
.pf-recent-head{
|
||||
display:none;
|
||||
}
|
||||
.pf-recent-row{
|
||||
display:grid;
|
||||
grid-template-columns:repeat(2,minmax(0,1fr));
|
||||
gap:10px 12px;
|
||||
padding:12px;
|
||||
border:1px solid var(--paper-2);
|
||||
border-radius:var(--radius);
|
||||
background:var(--bg-surface);
|
||||
}
|
||||
.pf-recent__learner{
|
||||
grid-column:1 / -1;
|
||||
padding-bottom:10px;
|
||||
border-bottom:1px solid var(--paper-2);
|
||||
}
|
||||
.pf-recent__learner code{
|
||||
white-space:normal;
|
||||
overflow-wrap:anywhere;
|
||||
}
|
||||
.pf-recent__cell{
|
||||
display:grid;
|
||||
grid-template-columns:minmax(74px,.4fr) minmax(0,1fr);
|
||||
gap:8px;
|
||||
align-items:center;
|
||||
}
|
||||
.pf-recent__cell--open{
|
||||
justify-content:stretch;
|
||||
}
|
||||
.pf-recent__cell::before{
|
||||
display:block;
|
||||
content:attr(data-label);
|
||||
color:var(--text-muted);
|
||||
font-size:var(--fs-xs);
|
||||
font-weight:700;
|
||||
line-height:1.35;
|
||||
}
|
||||
.pf-session{
|
||||
grid-template-columns:8px minmax(0,1fr);
|
||||
}
|
||||
.pf-session__meta{
|
||||
grid-column:2;
|
||||
justify-content:flex-start;
|
||||
}
|
||||
.pf-session__open{
|
||||
grid-column:2;
|
||||
grid-row:auto;
|
||||
justify-self:start;
|
||||
}
|
||||
}
|
||||
@media (max-width:520px){
|
||||
.pf-kpis{grid-template-columns:repeat(2,minmax(0,1fr));}
|
||||
.pf-kpi,
|
||||
.pf-kpi + .pf-kpi{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-persona__actions{
|
||||
display:grid;
|
||||
grid-template-columns:repeat(2,minmax(0,1fr));
|
||||
}
|
||||
.pf-persona__actions .vg-btn{
|
||||
width:100%;
|
||||
}
|
||||
.pf-growth-list{
|
||||
padding:10px;
|
||||
}
|
||||
.pf-growth-card__metrics{
|
||||
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);
|
||||
}
|
||||
.pf-alert__resource{
|
||||
grid-column:2;
|
||||
justify-items:start;
|
||||
}
|
||||
.pf-recent-list{
|
||||
padding:8px;
|
||||
}
|
||||
.pf-recent-row{
|
||||
grid-template-columns:1fr;
|
||||
gap:10px;
|
||||
}
|
||||
.pf-recent__cell{
|
||||
grid-template-columns:minmax(64px,.32fr) minmax(0,1fr);
|
||||
}
|
||||
.pf-recent__cell--open .pf-recent__open{
|
||||
justify-self:start;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
|
|||
1564
apps/web/src/pages/learner-home.css
Normal file
1564
apps/web/src/pages/learner-home.css
Normal file
File diff suppressed because it is too large
Load diff
61
apps/web/src/pages/login/LoginBrand.tsx
Normal file
61
apps/web/src/pages/login/LoginBrand.tsx
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import { Icon } from "../../components/ui/Icon";
|
||||
|
||||
interface LoginBrandProps {
|
||||
allowedDomains: string[];
|
||||
oauthChecking: boolean;
|
||||
}
|
||||
|
||||
function VignetteMark() {
|
||||
return (
|
||||
<span className="lg-mark" aria-hidden="true">
|
||||
<svg viewBox="0 0 26 26" width={26} height={26} fill="none">
|
||||
<circle cx="13" cy="13" r="11" stroke="currentColor" strokeWidth="2" />
|
||||
<path
|
||||
d="M8 14.5c1.4 1.7 3 2.5 5 2.5s3.6-.8 5-2.5"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<circle cx="13" cy="9" r="1.6" fill="var(--clay)" />
|
||||
</svg>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export function LoginBrand({ allowedDomains, oauthChecking }: LoginBrandProps) {
|
||||
return (
|
||||
<section className="lg-brand" aria-label="Vignette">
|
||||
<div className="lg-wordmark">
|
||||
<VignetteMark />
|
||||
<span>Vignette</span>
|
||||
</div>
|
||||
|
||||
<div className="lg-copy">
|
||||
<span className="lg-kicker">
|
||||
<span className="d" aria-hidden="true" />
|
||||
상담 시뮬레이션 학습
|
||||
</span>
|
||||
<h1>
|
||||
<span className="lg-highlight">실제 계정으로 들어가고</span>, <br />
|
||||
실제 회기만 남깁니다.
|
||||
</h1>
|
||||
<p>
|
||||
Vignette는 상담 연습, 교수자 피드백, 운영 관리 권한을 안전한 로그인 세션으로 분리합니다.
|
||||
브라우저에는 인증 토큰을 저장하지 않습니다.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="lg-policy">
|
||||
<span>
|
||||
<Icon name="shield" size={17} />
|
||||
허용 도메인
|
||||
</span>
|
||||
{allowedDomains.length ? (
|
||||
allowedDomains.map((domain) => <b key={domain}>{domain}</b>)
|
||||
) : (
|
||||
<b>{oauthChecking ? "확인 중" : "설정 필요"}</b>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
169
apps/web/src/pages/login/LoginPanel.tsx
Normal file
169
apps/web/src/pages/login/LoginPanel.tsx
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
import { Icon, type IconName } from "../../components/ui/Icon";
|
||||
import type { Role } from "../../lib/auth";
|
||||
|
||||
export interface LoginRoleOption {
|
||||
role: Role;
|
||||
label: string;
|
||||
desc: string;
|
||||
dotClass: string;
|
||||
}
|
||||
|
||||
interface LoginOAuthView {
|
||||
ready: boolean;
|
||||
primaryDomainLabel: string;
|
||||
secondaryDomainLabel: string;
|
||||
statusIcon: Extract<IconName, "alert" | "info">;
|
||||
statusMessage: string;
|
||||
onStart: () => void;
|
||||
}
|
||||
|
||||
interface LoginDevAccessView {
|
||||
ready: boolean;
|
||||
selected: Role;
|
||||
pending: boolean;
|
||||
options: LoginRoleOption[];
|
||||
onSelect: (role: Role) => void;
|
||||
onEnter: (role: Role) => void;
|
||||
}
|
||||
|
||||
interface LoginErrorView {
|
||||
message: string | null;
|
||||
reason: string | null;
|
||||
}
|
||||
|
||||
interface LoginPanelProps {
|
||||
oauth: LoginOAuthView;
|
||||
devAccess: LoginDevAccessView;
|
||||
error: LoginErrorView;
|
||||
}
|
||||
|
||||
function LoginProviderButton({
|
||||
variant,
|
||||
icon,
|
||||
label,
|
||||
subLabel,
|
||||
disabled,
|
||||
onClick,
|
||||
}: {
|
||||
variant: "primary" | "secondary";
|
||||
icon: Extract<IconName, "google" | "school">;
|
||||
label: string;
|
||||
subLabel: string;
|
||||
disabled: boolean;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
return (
|
||||
<button className={`lg-obtn ${variant}`} type="button" onClick={onClick} disabled={disabled}>
|
||||
<span className="ic">
|
||||
<Icon name={icon} size={19} strokeWidth={1.8} />
|
||||
</span>
|
||||
<span className="txt">
|
||||
{label}
|
||||
<span className="sub">{subLabel}</span>
|
||||
</span>
|
||||
<Icon name="chevron-right" size={18} strokeWidth={2} />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function LoginErrorNotice({ error }: { error: LoginErrorView }) {
|
||||
if (!error.message) return null;
|
||||
return (
|
||||
<p className="lg-error">
|
||||
{error.message}
|
||||
{error.reason ? <small>오류 코드: {error.reason}</small> : null}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
function LoginDevAccess({ devAccess, error }: { devAccess: LoginDevAccessView; error: LoginErrorView }) {
|
||||
if (!devAccess.ready) return null;
|
||||
|
||||
return (
|
||||
<div className="lg-dev">
|
||||
<div className="lg-devhead">
|
||||
<span>로컬 테스트</span>
|
||||
<small>보안 로그인 세션 사용</small>
|
||||
</div>
|
||||
<div className="lg-rolepick" role="radiogroup" aria-label="로컬 테스트 역할">
|
||||
{devAccess.options.map((opt) => (
|
||||
<button
|
||||
key={opt.role}
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={devAccess.selected === opt.role}
|
||||
className={`lg-roleopt ${devAccess.selected === opt.role ? "is-sel" : ""}`}
|
||||
onClick={() => devAccess.onSelect(opt.role)}
|
||||
>
|
||||
<span className={`d ${opt.dotClass}`} aria-hidden="true" />
|
||||
<span>
|
||||
<b>{opt.label}</b>
|
||||
<small>{opt.desc}</small>
|
||||
</span>
|
||||
{devAccess.selected === opt.role ? <Icon name="check" size={15} /> : null}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
className="lg-devbtn"
|
||||
type="button"
|
||||
onClick={() => devAccess.onEnter(devAccess.selected)}
|
||||
disabled={devAccess.pending}
|
||||
>
|
||||
{devAccess.pending ? "테스트 계정 확인 중" : "로컬 테스트 계정으로 계속"}
|
||||
</button>
|
||||
<LoginErrorNotice error={error} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function LoginPanel({ oauth, devAccess, error }: LoginPanelProps) {
|
||||
return (
|
||||
<section className="lg-enter" aria-label="로그인">
|
||||
<div className="lg-panel">
|
||||
<span className="lg-kicker">
|
||||
<span className="d" aria-hidden="true" />
|
||||
계정으로 시작
|
||||
</span>
|
||||
<h2>로그인</h2>
|
||||
<p className="lg-lead">
|
||||
학교 또는 승인된 Google 계정으로 접속하면 역할과 코호트 권한을 확인합니다.
|
||||
</p>
|
||||
|
||||
<div className="lg-actions">
|
||||
<LoginProviderButton
|
||||
variant="primary"
|
||||
icon="school"
|
||||
label="학교 Google 계정으로 계속"
|
||||
subLabel={oauth.primaryDomainLabel}
|
||||
disabled={!oauth.ready}
|
||||
onClick={oauth.onStart}
|
||||
/>
|
||||
<LoginProviderButton
|
||||
variant="secondary"
|
||||
icon="google"
|
||||
label="Google 계정으로 계속"
|
||||
subLabel={oauth.secondaryDomainLabel}
|
||||
disabled={!oauth.ready}
|
||||
onClick={oauth.onStart}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!oauth.ready ? (
|
||||
<div className="lg-config" role="status">
|
||||
<Icon name={oauth.statusIcon} size={17} />
|
||||
<span>{oauth.statusMessage}</span>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<LoginDevAccess devAccess={devAccess} error={error} />
|
||||
|
||||
{!devAccess.ready ? <LoginErrorNotice error={error} /> : null}
|
||||
|
||||
<p className="lg-note">
|
||||
교육용 비치료 연구 도구입니다. 실제 치료, 진단, 위기 개입을 대체하지 않습니다.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
484
apps/web/src/pages/login/login.css
Normal file
484
apps/web/src/pages/login/login.css
Normal file
|
|
@ -0,0 +1,484 @@
|
|||
.lg-root {
|
||||
min-height: 100dvh;
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(360px, 500px);
|
||||
background:
|
||||
linear-gradient(
|
||||
90deg,
|
||||
rgba(7, 16, 14, 0.84) 0%,
|
||||
rgba(10, 21, 19, 0.72) 48%,
|
||||
rgba(10, 21, 19, 0.55) 74%,
|
||||
rgba(10, 21, 19, 0.62) 100%
|
||||
),
|
||||
var(--asset-login-room) center / cover no-repeat;
|
||||
color: #edf4f2;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.lg-root::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(180deg, rgba(3, 9, 8, 0.18), rgba(3, 9, 8, 0.42));
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.lg-brand,
|
||||
.lg-enter {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.lg-brand {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
gap: var(--sp-7);
|
||||
padding: var(--sp-7);
|
||||
background: transparent;
|
||||
color: #edf4f2;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.lg-brand::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.lg-wordmark {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 19px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0;
|
||||
color: #edf4f2;
|
||||
}
|
||||
|
||||
.lg-mark {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: var(--accent-bright);
|
||||
}
|
||||
|
||||
.lg-copy {
|
||||
max-width: 620px;
|
||||
}
|
||||
|
||||
.lg-copy,
|
||||
.lg-policy {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.lg-kicker {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-family: var(--font-num);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent-bright);
|
||||
}
|
||||
|
||||
.lg-kicker .d {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
.lg-highlight {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
padding: 0 4px;
|
||||
background: linear-gradient(90deg, #59b5a6, #8ee4d6);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.lg-copy h1 {
|
||||
margin: var(--sp-4) 0 0;
|
||||
max-width: 640px;
|
||||
font-size: 56px;
|
||||
line-height: 1.12;
|
||||
letter-spacing: 0;
|
||||
font-weight: 760;
|
||||
}
|
||||
|
||||
.lg-copy p {
|
||||
margin: var(--sp-5) 0 0;
|
||||
max-width: 560px;
|
||||
color: rgba(237, 244, 242, 0.72);
|
||||
font-size: 17px;
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
.lg-policy {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
width: max-content;
|
||||
max-width: min(430px, 100%);
|
||||
padding: 14px 16px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.14);
|
||||
border-radius: var(--radius-lg);
|
||||
background: rgba(237, 244, 242, 0.08);
|
||||
backdrop-filter: blur(18px) saturate(1.08);
|
||||
-webkit-backdrop-filter: blur(18px) saturate(1.08);
|
||||
color: rgba(237, 244, 242, 0.66);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.lg-policy span,
|
||||
.lg-policy b {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.lg-policy b {
|
||||
color: #edf4f2;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-radius: 999px;
|
||||
padding: 5px 10px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.lg-enter {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--sp-6);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.lg-panel {
|
||||
width: 100%;
|
||||
max-width: 430px;
|
||||
background: rgba(11, 25, 22, 0.64);
|
||||
border: 1px solid rgba(255, 255, 255, 0.16);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: 0 24px 70px rgba(0, 0, 0, 0.34), inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
||||
backdrop-filter: blur(24px) saturate(1.14);
|
||||
-webkit-backdrop-filter: blur(24px) saturate(1.14);
|
||||
padding: var(--sp-6);
|
||||
color: #edf4f2;
|
||||
}
|
||||
|
||||
.lg-panel h2 {
|
||||
margin: var(--sp-3) 0 0;
|
||||
font-size: 28px;
|
||||
line-height: 1.25;
|
||||
letter-spacing: 0;
|
||||
color: #f7fbf9;
|
||||
}
|
||||
|
||||
.lg-lead {
|
||||
margin: 10px 0 0;
|
||||
color: rgba(237, 244, 242, 0.76);
|
||||
font-size: 14px;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.lg-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-3);
|
||||
margin-top: var(--sp-6);
|
||||
}
|
||||
|
||||
.lg-obtn {
|
||||
width: 100%;
|
||||
min-height: 58px;
|
||||
display: grid;
|
||||
grid-template-columns: 36px minmax(0, 1fr) 18px;
|
||||
align-items: center;
|
||||
gap: 13px;
|
||||
border-radius: var(--radius);
|
||||
padding: 11px 14px;
|
||||
font-family: var(--font-sans);
|
||||
font-size: 15px;
|
||||
font-weight: 650;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.lg-obtn.primary {
|
||||
background: rgba(89, 181, 166, 0.2);
|
||||
border: 1px solid rgba(114, 211, 197, 0.44);
|
||||
color: #f7fbf9;
|
||||
}
|
||||
|
||||
.lg-obtn.primary:hover {
|
||||
background: rgba(89, 181, 166, 0.28);
|
||||
border-color: rgba(114, 211, 197, 0.62);
|
||||
}
|
||||
|
||||
.lg-obtn.secondary {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border: 1px solid rgba(255, 255, 255, 0.13);
|
||||
color: #edf4f2;
|
||||
}
|
||||
|
||||
.lg-obtn.secondary:hover {
|
||||
border-color: rgba(114, 211, 197, 0.42);
|
||||
background: rgba(255, 255, 255, 0.11);
|
||||
}
|
||||
|
||||
.lg-obtn:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 1;
|
||||
background: rgba(255, 255, 255, 0.07);
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
color: rgba(237, 244, 242, 0.58);
|
||||
}
|
||||
|
||||
.lg-obtn:disabled:hover {
|
||||
background: rgba(255, 255, 255, 0.07);
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.lg-obtn:disabled .ic {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: rgba(237, 244, 242, 0.58);
|
||||
}
|
||||
|
||||
.lg-obtn:disabled .sub {
|
||||
color: rgba(237, 244, 242, 0.45);
|
||||
}
|
||||
|
||||
.lg-obtn .ic {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.16);
|
||||
}
|
||||
|
||||
.lg-obtn.secondary .ic {
|
||||
background: rgba(7, 17, 15, 0.34);
|
||||
}
|
||||
|
||||
.lg-obtn .txt {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.lg-obtn .sub {
|
||||
font-size: 12px;
|
||||
font-weight: 550;
|
||||
color: rgba(237, 244, 242, 0.58);
|
||||
}
|
||||
|
||||
.lg-obtn.primary .sub {
|
||||
color: rgba(251, 250, 248, 0.74);
|
||||
}
|
||||
|
||||
.lg-config {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
margin-top: var(--sp-3);
|
||||
padding: 10px 12px;
|
||||
border-radius: var(--radius);
|
||||
background: rgba(154, 94, 20, 0.28);
|
||||
border: 1px solid rgba(236, 180, 91, 0.16);
|
||||
color: #f2b75f;
|
||||
font-size: 12.5px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.lg-dev {
|
||||
margin-top: var(--sp-6);
|
||||
padding-top: var(--sp-5);
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.lg-devhead {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: var(--sp-3);
|
||||
color: #edf4f2;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.lg-devhead small {
|
||||
color: rgba(237, 244, 242, 0.58);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.lg-rolepick {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: var(--sp-2);
|
||||
margin-top: var(--sp-3);
|
||||
}
|
||||
|
||||
.lg-roleopt {
|
||||
min-height: 50px;
|
||||
display: grid;
|
||||
grid-template-columns: 10px minmax(0, 1fr) 16px;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 9px 11px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.11);
|
||||
border-radius: var(--radius);
|
||||
background: rgba(255, 255, 255, 0.045);
|
||||
color: #edf4f2;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.lg-roleopt.is-sel {
|
||||
border-color: rgba(114, 211, 197, 0.58);
|
||||
background: rgba(89, 181, 166, 0.15);
|
||||
}
|
||||
|
||||
.lg-roleopt .d {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.lg-roleopt .d.learner {
|
||||
background: var(--accent-bright);
|
||||
}
|
||||
|
||||
.lg-roleopt .d.teacher {
|
||||
background: #5478c4;
|
||||
}
|
||||
|
||||
.lg-roleopt .d.admin {
|
||||
background: #7d818e;
|
||||
}
|
||||
|
||||
.lg-roleopt b {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.lg-roleopt small {
|
||||
display: block;
|
||||
margin-top: 1px;
|
||||
color: rgba(237, 244, 242, 0.58);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.lg-devbtn {
|
||||
width: 100%;
|
||||
min-height: 46px;
|
||||
margin-top: var(--sp-3);
|
||||
border: 1px solid rgba(114, 211, 197, 0.58);
|
||||
border-radius: var(--radius);
|
||||
background: rgba(89, 181, 166, 0.15);
|
||||
color: #8fe7d9;
|
||||
font-family: var(--font-sans);
|
||||
font-size: 14px;
|
||||
font-weight: 750;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.lg-devbtn:disabled {
|
||||
opacity: 0.62;
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
.lg-error {
|
||||
margin: var(--sp-3) 0 0;
|
||||
color: var(--crit-text);
|
||||
background: rgba(122, 38, 38, 0.28);
|
||||
border: 1px solid rgba(255, 145, 145, 0.18);
|
||||
border-radius: var(--radius);
|
||||
padding: 10px 12px;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.lg-error small {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
color: rgba(237, 244, 242, 0.56);
|
||||
font-family: var(--font-num);
|
||||
font-size: 11.5px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.lg-note {
|
||||
margin: var(--sp-5) 0 0;
|
||||
color: rgba(237, 244, 242, 0.52);
|
||||
font-size: 12.5px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
@media (max-width: 880px) {
|
||||
.lg-root {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.lg-root::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.lg-brand {
|
||||
padding: var(--sp-6) var(--sp-5);
|
||||
gap: var(--sp-6);
|
||||
}
|
||||
|
||||
.lg-brand::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.lg-copy h1 {
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.lg-enter {
|
||||
padding: var(--sp-5);
|
||||
}
|
||||
|
||||
.lg-panel {
|
||||
max-width: 560px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.lg-brand {
|
||||
padding: var(--sp-5);
|
||||
}
|
||||
|
||||
.lg-copy h1 {
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.lg-copy p {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.lg-enter {
|
||||
padding: var(--sp-4);
|
||||
}
|
||||
|
||||
.lg-panel {
|
||||
padding: var(--sp-5);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,134 +1,253 @@
|
|||
.ob-root {
|
||||
width: min(100%, 1040px);
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
.ob-page{
|
||||
min-height:100dvh;
|
||||
width:100%;
|
||||
background:var(--bg-app);
|
||||
color:var(--text-body);
|
||||
padding:clamp(20px,5vw,56px);
|
||||
}
|
||||
|
||||
.ob-head h1 {
|
||||
margin: 6px 0 0;
|
||||
color: var(--text-strong);
|
||||
font-size: var(--fs-h2);
|
||||
line-height: 1.28;
|
||||
letter-spacing: 0;
|
||||
.ob-shell{
|
||||
width:100%;
|
||||
max-width:900px;
|
||||
margin:0 auto;
|
||||
display:grid;
|
||||
gap:var(--sp-6);
|
||||
}
|
||||
|
||||
.ob-head p {
|
||||
margin: 8px 0 0;
|
||||
color: var(--text-body);
|
||||
font-size: var(--fs-sm);
|
||||
line-height: 1.55;
|
||||
.ob-head{
|
||||
display:grid;
|
||||
gap:8px;
|
||||
}
|
||||
|
||||
.ob-alert {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px 14px;
|
||||
border-radius: var(--radius);
|
||||
background: var(--crit-tint);
|
||||
color: var(--crit-text);
|
||||
font-size: var(--fs-sm);
|
||||
.ob-head p{
|
||||
margin:0;
|
||||
color:var(--accent-deep);
|
||||
font-size:12px;
|
||||
font-weight:800;
|
||||
}
|
||||
|
||||
.ob-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(280px, 360px);
|
||||
gap: 14px;
|
||||
align-items: start;
|
||||
.ob-head h1{
|
||||
margin:0;
|
||||
color:var(--text-strong);
|
||||
font-size:clamp(28px,4vw,44px);
|
||||
line-height:1.18;
|
||||
letter-spacing:0;
|
||||
}
|
||||
|
||||
.ob-panel {
|
||||
min-width: 0;
|
||||
border: 1px solid var(--hair);
|
||||
border-radius: var(--radius);
|
||||
background: var(--bg-surface);
|
||||
box-shadow: var(--shadow-sm);
|
||||
.ob-form{
|
||||
min-width:0;
|
||||
display:grid;
|
||||
gap:var(--sp-6);
|
||||
}
|
||||
|
||||
.ob-form {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
.ob-section{
|
||||
min-width:0;
|
||||
display:grid;
|
||||
gap:var(--sp-4);
|
||||
padding-bottom:var(--sp-5);
|
||||
border-bottom:1px solid var(--border-subtle);
|
||||
}
|
||||
|
||||
.ob-form .vg-btn,
|
||||
.ob-check {
|
||||
grid-column: 1 / -1;
|
||||
.ob-section__head h2{
|
||||
margin:0;
|
||||
color:var(--text-strong);
|
||||
font-size:18px;
|
||||
line-height:1.35;
|
||||
letter-spacing:0;
|
||||
}
|
||||
|
||||
.ob-check {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
color: var(--text-body);
|
||||
font-size: var(--fs-sm);
|
||||
line-height: 1.4;
|
||||
.ob-avatar{
|
||||
display:grid;
|
||||
grid-template-columns:auto minmax(0,1fr);
|
||||
gap:var(--sp-3);
|
||||
align-items:center;
|
||||
}
|
||||
|
||||
.ob-check input {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
accent-color: var(--accent);
|
||||
.ob-avatar__preview{
|
||||
width:72px;
|
||||
height:72px;
|
||||
border-radius:50%;
|
||||
display:grid;
|
||||
place-items:center;
|
||||
overflow:hidden;
|
||||
background:var(--accent-tint);
|
||||
color:var(--accent-deep);
|
||||
font-size:28px;
|
||||
font-weight:800;
|
||||
border:1px solid var(--border-subtle);
|
||||
}
|
||||
|
||||
.ob-docs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
.ob-avatar__preview img{
|
||||
width:100%;
|
||||
height:100%;
|
||||
display:block;
|
||||
object-fit:cover;
|
||||
}
|
||||
|
||||
.ob-docs h2 {
|
||||
margin: 6px 0 0;
|
||||
color: var(--text-strong);
|
||||
font-size: var(--fs-body);
|
||||
.ob-avatar__body{
|
||||
min-width:0;
|
||||
display:grid;
|
||||
gap:7px;
|
||||
}
|
||||
|
||||
.ob-docs p {
|
||||
margin: 6px 0 0;
|
||||
color: var(--text-body);
|
||||
font-size: var(--fs-xs);
|
||||
line-height: 1.55;
|
||||
.ob-avatar__body span{
|
||||
color:var(--text-strong);
|
||||
font-size:13px;
|
||||
font-weight:780;
|
||||
}
|
||||
|
||||
.ob-docs article {
|
||||
min-width: 0;
|
||||
max-height: 220px;
|
||||
overflow: auto;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--hair);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-surface-2);
|
||||
.ob-avatar__body p{
|
||||
margin:0;
|
||||
color:var(--text-muted);
|
||||
font-size:12.5px;
|
||||
line-height:1.45;
|
||||
}
|
||||
|
||||
.ob-docs b,
|
||||
.ob-docs small {
|
||||
display: block;
|
||||
.ob-avatar__button{
|
||||
position:relative;
|
||||
width:max-content;
|
||||
min-height:34px;
|
||||
display:inline-flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
padding:0 12px;
|
||||
border:1px solid var(--border-subtle);
|
||||
border-radius:var(--radius-sm);
|
||||
background:var(--bg-surface);
|
||||
color:var(--text-strong);
|
||||
font-size:12px;
|
||||
font-weight:760;
|
||||
cursor:pointer;
|
||||
}
|
||||
|
||||
.ob-docs b {
|
||||
color: var(--text-strong);
|
||||
font-size: var(--fs-sm);
|
||||
.ob-avatar__button input{
|
||||
position:absolute;
|
||||
inline-size:1px;
|
||||
block-size:1px;
|
||||
opacity:0;
|
||||
pointer-events:none;
|
||||
}
|
||||
|
||||
.ob-docs small {
|
||||
margin-top: 2px;
|
||||
color: var(--text-muted);
|
||||
font-family: var(--font-num);
|
||||
font-size: var(--fs-xs);
|
||||
.ob-field--wide{
|
||||
grid-column:1 / -1;
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.ob-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 560px) {
|
||||
.ob-form {
|
||||
grid-template-columns: 1fr;
|
||||
.ob-fields{
|
||||
display:grid;
|
||||
grid-template-columns:repeat(2,minmax(0,1fr));
|
||||
gap:var(--sp-3);
|
||||
}
|
||||
.ob-form label{
|
||||
min-width:0;
|
||||
display:grid;
|
||||
gap:7px;
|
||||
}
|
||||
.ob-form label span{
|
||||
color:var(--text-muted);
|
||||
font-size:12px;
|
||||
font-weight:730;
|
||||
}
|
||||
.ob-form input[type="text"],
|
||||
.ob-form input:not([type]){
|
||||
min-width:0;
|
||||
}
|
||||
.ob-form input,
|
||||
.ob-form textarea{
|
||||
width:100%;
|
||||
border:1px solid var(--border-subtle);
|
||||
border-radius:var(--radius-sm);
|
||||
background:var(--bg-surface-2);
|
||||
color:var(--text-strong);
|
||||
padding:0 12px;
|
||||
font:500 var(--fs-sm)/1.2 var(--font-sans);
|
||||
}
|
||||
.ob-form input{
|
||||
min-height:42px;
|
||||
}
|
||||
.ob-form textarea{
|
||||
min-height:92px;
|
||||
padding:11px 12px;
|
||||
resize:vertical;
|
||||
line-height:1.45;
|
||||
}
|
||||
.ob-form input:focus,
|
||||
.ob-form textarea:focus{
|
||||
outline:2px solid color-mix(in srgb,var(--accent) 24%,transparent);
|
||||
border-color:var(--accent);
|
||||
}
|
||||
.ob-checks{
|
||||
display:grid;
|
||||
gap:10px;
|
||||
padding:14px;
|
||||
border:1px solid var(--border-subtle);
|
||||
border-radius:var(--radius);
|
||||
background:var(--bg-surface-2);
|
||||
}
|
||||
.ob-checks label{
|
||||
grid-template-columns:auto minmax(0,1fr);
|
||||
align-items:center;
|
||||
gap:10px;
|
||||
}
|
||||
.ob-checks input{
|
||||
width:18px;
|
||||
height:18px;
|
||||
min-height:18px;
|
||||
padding:0;
|
||||
accent-color:var(--accent);
|
||||
}
|
||||
.ob-legal{
|
||||
min-width:0;
|
||||
display:grid;
|
||||
gap:8px;
|
||||
}
|
||||
.ob-legal details{
|
||||
min-width:0;
|
||||
border:1px solid var(--border-subtle);
|
||||
border-radius:var(--radius-sm);
|
||||
background:var(--bg-surface-2);
|
||||
}
|
||||
.ob-legal summary{
|
||||
min-width:0;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:space-between;
|
||||
gap:10px;
|
||||
min-height:42px;
|
||||
padding:0 12px;
|
||||
cursor:pointer;
|
||||
}
|
||||
.ob-legal summary span{
|
||||
min-width:0;
|
||||
color:var(--text-strong);
|
||||
font-size:13px;
|
||||
font-weight:760;
|
||||
overflow:hidden;
|
||||
text-overflow:ellipsis;
|
||||
white-space:nowrap;
|
||||
}
|
||||
.ob-legal summary b{
|
||||
flex:none;
|
||||
color:var(--text-muted);
|
||||
font-size:11px;
|
||||
font-weight:700;
|
||||
}
|
||||
.ob-note,
|
||||
.ob-error{
|
||||
margin:0;
|
||||
color:var(--text-muted);
|
||||
font-size:12.5px;
|
||||
line-height:1.5;
|
||||
}
|
||||
.ob-doc-text{
|
||||
max-height:260px;
|
||||
overflow:auto;
|
||||
white-space:pre-wrap;
|
||||
color:var(--text-body);
|
||||
font-size:12.5px;
|
||||
line-height:1.55;
|
||||
padding:0 12px 12px;
|
||||
}
|
||||
.ob-error{
|
||||
color:var(--crit-text);
|
||||
}
|
||||
.ob-actions{
|
||||
display:flex;
|
||||
justify-content:flex-start;
|
||||
}
|
||||
@media (max-width:620px){
|
||||
.ob-page{
|
||||
padding:14px;
|
||||
}
|
||||
.ob-fields{
|
||||
grid-template-columns:1fr;
|
||||
}
|
||||
.ob-avatar{
|
||||
grid-template-columns:1fr;
|
||||
}
|
||||
.ob-actions .vg-btn{
|
||||
width:100%;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
94
apps/web/src/pages/pending-approval.css
Normal file
94
apps/web/src/pages/pending-approval.css
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
.pa-page{
|
||||
min-height:100dvh;
|
||||
display:grid;
|
||||
place-items:center;
|
||||
padding:clamp(18px,5vw,56px);
|
||||
background:var(--bg-app);
|
||||
color:var(--text-body);
|
||||
}
|
||||
.pa-panel{
|
||||
width:min(100%,620px);
|
||||
display:grid;
|
||||
gap:var(--sp-4);
|
||||
padding:clamp(24px,5vw,44px);
|
||||
border:1px solid var(--border-subtle);
|
||||
border-radius:var(--radius);
|
||||
background:color-mix(in srgb,var(--bg-surface) 92%,white 8%);
|
||||
box-shadow:0 18px 50px rgba(28,43,40,.10);
|
||||
}
|
||||
.pa-mark{
|
||||
width:62px;
|
||||
height:62px;
|
||||
display:grid;
|
||||
place-items:center;
|
||||
border-radius:var(--radius);
|
||||
background:var(--accent-tint);
|
||||
color:var(--accent-deep);
|
||||
border:1px solid color-mix(in srgb,var(--accent) 20%,transparent);
|
||||
}
|
||||
.pa-kicker{
|
||||
margin:0;
|
||||
color:var(--accent-deep);
|
||||
font-size:12px;
|
||||
font-weight:820;
|
||||
}
|
||||
.pa-panel h1{
|
||||
margin:0;
|
||||
color:var(--text-strong);
|
||||
font-size:clamp(30px,5vw,46px);
|
||||
line-height:1.17;
|
||||
letter-spacing:0;
|
||||
}
|
||||
.pa-copy{
|
||||
margin:0;
|
||||
color:var(--text-muted);
|
||||
font-size:15px;
|
||||
line-height:1.7;
|
||||
}
|
||||
.pa-copy b{
|
||||
color:var(--text-strong);
|
||||
font-weight:760;
|
||||
}
|
||||
.pa-status{
|
||||
min-width:0;
|
||||
display:grid;
|
||||
grid-template-columns:auto minmax(0,1fr);
|
||||
gap:12px;
|
||||
align-items:center;
|
||||
padding:14px;
|
||||
border:1px solid var(--border-subtle);
|
||||
border-radius:var(--radius);
|
||||
background:var(--bg-surface-2);
|
||||
}
|
||||
.pa-status > span{
|
||||
width:10px;
|
||||
height:10px;
|
||||
border-radius:50%;
|
||||
background:#d89a2b;
|
||||
box-shadow:0 0 0 5px rgba(216,154,43,.14);
|
||||
}
|
||||
.pa-status b{
|
||||
display:block;
|
||||
color:var(--text-strong);
|
||||
font-size:14px;
|
||||
}
|
||||
.pa-status small{
|
||||
display:block;
|
||||
margin-top:3px;
|
||||
color:var(--text-muted);
|
||||
font-size:12.5px;
|
||||
line-height:1.45;
|
||||
}
|
||||
.pa-actions{
|
||||
display:flex;
|
||||
flex-wrap:wrap;
|
||||
gap:10px;
|
||||
}
|
||||
@media (max-width:560px){
|
||||
.pa-panel{
|
||||
border-radius:var(--radius);
|
||||
}
|
||||
.pa-actions .vg-btn{
|
||||
width:100%;
|
||||
}
|
||||
}
|
||||
688
apps/web/src/pages/persona-studio.css
Normal file
688
apps/web/src/pages/persona-studio.css
Normal file
|
|
@ -0,0 +1,688 @@
|
|||
.ps-root{
|
||||
display:grid;
|
||||
gap:20px;
|
||||
}
|
||||
.ps-head{
|
||||
display:flex;
|
||||
justify-content:space-between;
|
||||
align-items:flex-start;
|
||||
gap:16px;
|
||||
}
|
||||
.ps-head h1{
|
||||
margin:4px 0 0;
|
||||
font-size:clamp(24px,3vw,38px);
|
||||
line-height:1.16;
|
||||
letter-spacing:0;
|
||||
color:var(--text-strong);
|
||||
}
|
||||
.ps-head__actions{
|
||||
display:flex;
|
||||
gap:8px;
|
||||
flex-wrap:wrap;
|
||||
justify-content:flex-end;
|
||||
}
|
||||
.ps-error{
|
||||
display:flex;
|
||||
align-items:center;
|
||||
gap:8px;
|
||||
padding:12px 14px;
|
||||
border:1px solid var(--crit-solid);
|
||||
border-radius:var(--radius);
|
||||
color:var(--crit-text);
|
||||
background:var(--crit-tint);
|
||||
}
|
||||
.ps-layout{
|
||||
display:grid;
|
||||
grid-template-columns:minmax(220px,300px) minmax(460px,1fr) minmax(240px,320px);
|
||||
gap:16px;
|
||||
align-items:start;
|
||||
}
|
||||
.ps-rail,
|
||||
.ps-inspector{
|
||||
position:sticky;
|
||||
top:76px;
|
||||
display:grid;
|
||||
gap:14px;
|
||||
min-width:0;
|
||||
}
|
||||
.ps-rail__head,
|
||||
.ps-editor__top,
|
||||
.ps-workflow,
|
||||
.ps-inspector__block{
|
||||
border:1px solid var(--border-subtle);
|
||||
border-radius:var(--radius);
|
||||
background:var(--bg-surface);
|
||||
padding:16px;
|
||||
box-shadow:var(--shadow-sm);
|
||||
}
|
||||
.ps-rail__head,
|
||||
.ps-editor__top{
|
||||
display:flex;
|
||||
align-items:flex-start;
|
||||
justify-content:space-between;
|
||||
gap:12px;
|
||||
}
|
||||
.ps-rail__head b,
|
||||
.ps-editor__top h2{
|
||||
display:block;
|
||||
margin:4px 0 0;
|
||||
color:var(--text-strong);
|
||||
}
|
||||
.ps-editor__top h2{
|
||||
font-size:22px;
|
||||
line-height:1.25;
|
||||
letter-spacing:0;
|
||||
}
|
||||
.ps-list,
|
||||
.ps-approved,
|
||||
.ps-review-cards,
|
||||
.ps-check{
|
||||
display:grid;
|
||||
gap:8px;
|
||||
}
|
||||
.ps-list-row{
|
||||
width:100%;
|
||||
display:grid;
|
||||
grid-template-columns:42px minmax(0,1fr);
|
||||
gap:10px;
|
||||
align-items:center;
|
||||
padding:10px;
|
||||
border:1px solid var(--border-subtle);
|
||||
border-radius:var(--radius);
|
||||
background:var(--bg-surface);
|
||||
color:inherit;
|
||||
text-align:left;
|
||||
cursor:pointer;
|
||||
}
|
||||
.ps-list-row:hover,
|
||||
.ps-list-row.is-active{
|
||||
border-color:var(--accent);
|
||||
background:var(--accent-tint);
|
||||
}
|
||||
.ps-list-row:disabled{
|
||||
opacity:.55;
|
||||
cursor:not-allowed;
|
||||
}
|
||||
.ps-list-row__code{
|
||||
display:grid;
|
||||
place-items:center;
|
||||
min-width:38px;
|
||||
min-height:34px;
|
||||
border:1px solid var(--hair);
|
||||
border-radius:var(--radius-sm);
|
||||
color:var(--accent-deep);
|
||||
background:var(--bg-surface-2);
|
||||
font-weight:760;
|
||||
font-family:var(--font-num);
|
||||
}
|
||||
.ps-list-row__body{
|
||||
min-width:0;
|
||||
}
|
||||
.ps-list-row__body b,
|
||||
.ps-list-row__body small{
|
||||
display:block;
|
||||
overflow:hidden;
|
||||
text-overflow:ellipsis;
|
||||
white-space:nowrap;
|
||||
}
|
||||
.ps-list-row__body small,
|
||||
.ps-muted,
|
||||
.ps-approved-row span,
|
||||
.ps-review span{
|
||||
color:var(--text-muted);
|
||||
font-size:var(--fs-xs);
|
||||
}
|
||||
.ps-approved{
|
||||
padding:14px;
|
||||
border:1px solid var(--border-subtle);
|
||||
border-radius:var(--radius);
|
||||
background:var(--bg-surface);
|
||||
}
|
||||
.ps-workflow{
|
||||
display:grid;
|
||||
gap:10px;
|
||||
}
|
||||
.ps-workflow-step{
|
||||
display:grid;
|
||||
grid-template-columns:30px minmax(0,1fr);
|
||||
align-items:center;
|
||||
gap:10px;
|
||||
padding:10px;
|
||||
border:1px solid var(--hair);
|
||||
border-radius:var(--radius-sm);
|
||||
background:var(--bg-surface-2);
|
||||
color:var(--text-muted);
|
||||
}
|
||||
.ps-workflow-step > span{
|
||||
display:grid;
|
||||
place-items:center;
|
||||
width:30px;
|
||||
height:30px;
|
||||
border:1px solid var(--border-subtle);
|
||||
border-radius:50%;
|
||||
background:var(--bg-surface);
|
||||
color:var(--text-muted);
|
||||
font-family:var(--font-num);
|
||||
font-weight:760;
|
||||
}
|
||||
.ps-workflow-step b,
|
||||
.ps-workflow-step small{
|
||||
display:block;
|
||||
overflow:hidden;
|
||||
text-overflow:ellipsis;
|
||||
white-space:nowrap;
|
||||
}
|
||||
.ps-workflow-step b{
|
||||
color:var(--text-strong);
|
||||
font-size:var(--fs-sm);
|
||||
}
|
||||
.ps-workflow-step small{
|
||||
margin-top:2px;
|
||||
font-size:11px;
|
||||
}
|
||||
.ps-workflow-step.is-current{
|
||||
border-color:var(--accent);
|
||||
background:var(--accent-tint);
|
||||
color:var(--accent-deep);
|
||||
}
|
||||
.ps-workflow-step.is-current > span,
|
||||
.ps-workflow-step.is-done > span{
|
||||
border-color:var(--accent);
|
||||
background:var(--accent);
|
||||
color:var(--text-on-accent);
|
||||
}
|
||||
.ps-approved-row{
|
||||
display:grid;
|
||||
grid-template-columns:38px minmax(0,1fr);
|
||||
gap:8px 10px;
|
||||
align-items:center;
|
||||
padding:8px 0;
|
||||
border-top:1px solid var(--hair);
|
||||
}
|
||||
.ps-approved-row:first-of-type{border-top:0;}
|
||||
.ps-approved-row__code{
|
||||
font-family:var(--font-num);
|
||||
font-size:var(--fs-xs);
|
||||
color:var(--text-muted);
|
||||
}
|
||||
.ps-approved-row__body{
|
||||
min-width:0;
|
||||
display:grid;
|
||||
gap:2px;
|
||||
}
|
||||
.ps-approved-row b{
|
||||
min-width:0;
|
||||
overflow:hidden;
|
||||
text-overflow:ellipsis;
|
||||
white-space:nowrap;
|
||||
font-size:var(--fs-xs);
|
||||
}
|
||||
.ps-approved-row small{
|
||||
min-width:0;
|
||||
overflow:hidden;
|
||||
text-overflow:ellipsis;
|
||||
white-space:nowrap;
|
||||
color:var(--text-muted);
|
||||
font-size:11px;
|
||||
}
|
||||
.ps-approved-row__actions{
|
||||
grid-column:1 / -1;
|
||||
display:grid;
|
||||
grid-template-columns:repeat(2,minmax(0,1fr));
|
||||
gap:6px;
|
||||
}
|
||||
.ps-approved-row__actions .vg-btn{
|
||||
width:100%;
|
||||
}
|
||||
.ps-editor{
|
||||
display:grid;
|
||||
gap:14px;
|
||||
min-width:0;
|
||||
}
|
||||
.ps-source-panel{
|
||||
display:grid;
|
||||
gap:12px;
|
||||
padding:16px;
|
||||
}
|
||||
.ps-source-panel__head,
|
||||
.ps-source-panel__actions{
|
||||
display:flex;
|
||||
align-items:flex-start;
|
||||
justify-content:space-between;
|
||||
gap:12px;
|
||||
}
|
||||
.ps-source-panel__head b{
|
||||
display:block;
|
||||
margin-top:4px;
|
||||
color:var(--text-strong);
|
||||
font-size:var(--fs-body);
|
||||
}
|
||||
.ps-source-grid{
|
||||
display:grid;
|
||||
grid-template-columns:minmax(220px,.42fr) minmax(280px,.58fr);
|
||||
gap:12px;
|
||||
}
|
||||
.ps-source-grid .ps-field--wide{
|
||||
grid-column:1 / -1;
|
||||
}
|
||||
.ps-upload-card{
|
||||
display:grid;
|
||||
grid-template-columns:minmax(0,1fr) auto;
|
||||
align-items:center;
|
||||
gap:14px;
|
||||
padding:16px;
|
||||
border:1px dashed var(--accent);
|
||||
border-radius:var(--radius);
|
||||
background:var(--accent-tint);
|
||||
}
|
||||
.ps-upload-card b,
|
||||
.ps-upload-card span{
|
||||
display:block;
|
||||
}
|
||||
.ps-upload-card b{
|
||||
margin-top:5px;
|
||||
color:var(--text-strong);
|
||||
overflow:hidden;
|
||||
text-overflow:ellipsis;
|
||||
white-space:nowrap;
|
||||
}
|
||||
.ps-upload-card span{
|
||||
margin-top:4px;
|
||||
color:var(--text-muted);
|
||||
font-size:var(--fs-xs);
|
||||
line-height:1.45;
|
||||
}
|
||||
.ps-file-input{
|
||||
display:none;
|
||||
}
|
||||
.ps-source-panel__actions span{
|
||||
min-width:0;
|
||||
color:var(--text-muted);
|
||||
font-size:var(--fs-xs);
|
||||
line-height:1.45;
|
||||
}
|
||||
.ps-tabs,
|
||||
.ps-segments{
|
||||
display:flex;
|
||||
flex-wrap:wrap;
|
||||
gap:6px;
|
||||
}
|
||||
.ps-tabs button,
|
||||
.ps-segments button{
|
||||
min-height:34px;
|
||||
padding:0 12px;
|
||||
border:1px solid var(--border-subtle);
|
||||
border-radius:var(--radius-sm);
|
||||
background:var(--bg-surface);
|
||||
color:var(--text-body);
|
||||
font-weight:700;
|
||||
cursor:pointer;
|
||||
}
|
||||
.ps-tabs button.is-active,
|
||||
.ps-segments button.is-active{
|
||||
border-color:var(--accent);
|
||||
color:var(--accent-deep);
|
||||
background:var(--accent-tint);
|
||||
}
|
||||
.ps-edit-panel{
|
||||
padding:0;
|
||||
overflow:hidden;
|
||||
}
|
||||
.ps-guidance{
|
||||
display:grid;
|
||||
grid-template-columns:minmax(0,.95fr) minmax(260px,.75fr);
|
||||
gap:16px;
|
||||
padding:16px;
|
||||
border-bottom:1px solid var(--hair);
|
||||
background:var(--bg-surface-2);
|
||||
}
|
||||
.ps-guidance b{
|
||||
display:block;
|
||||
margin-top:5px;
|
||||
color:var(--text-strong);
|
||||
line-height:1.35;
|
||||
}
|
||||
.ps-guidance p{
|
||||
margin:6px 0 0;
|
||||
color:var(--text-muted);
|
||||
font-size:var(--fs-xs);
|
||||
line-height:1.5;
|
||||
}
|
||||
.ps-guidance ul{
|
||||
display:grid;
|
||||
gap:7px;
|
||||
margin:0;
|
||||
padding:0;
|
||||
list-style:none;
|
||||
}
|
||||
.ps-guidance li{
|
||||
display:grid;
|
||||
grid-template-columns:8px minmax(0,1fr);
|
||||
align-items:start;
|
||||
gap:8px;
|
||||
color:var(--text-body);
|
||||
font-size:var(--fs-xs);
|
||||
line-height:1.45;
|
||||
}
|
||||
.ps-guidance li::before{
|
||||
content:"";
|
||||
width:6px;
|
||||
height:6px;
|
||||
margin-top:.55em;
|
||||
border-radius:50%;
|
||||
background:var(--accent);
|
||||
}
|
||||
.ps-form-grid{
|
||||
display:grid;
|
||||
grid-template-columns:repeat(2,minmax(0,1fr));
|
||||
gap:14px;
|
||||
padding:16px;
|
||||
}
|
||||
.ps-number-grid{
|
||||
display:grid;
|
||||
grid-template-columns:repeat(4,minmax(0,1fr));
|
||||
gap:10px;
|
||||
padding:16px;
|
||||
}
|
||||
.ps-field,
|
||||
.ps-numfield{
|
||||
display:grid;
|
||||
gap:7px;
|
||||
min-width:0;
|
||||
}
|
||||
.ps-list-field{
|
||||
display:grid;
|
||||
gap:9px;
|
||||
min-width:0;
|
||||
}
|
||||
.ps-field--wide{
|
||||
grid-column:1 / -1;
|
||||
}
|
||||
.ps-field span,
|
||||
.ps-numfield span,
|
||||
.ps-list-field__head span{
|
||||
color:var(--text-muted);
|
||||
font-size:var(--fs-xs);
|
||||
font-weight:760;
|
||||
}
|
||||
.ps-list-field__head{
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:space-between;
|
||||
gap:10px;
|
||||
min-width:0;
|
||||
}
|
||||
.ps-list-field__rows{
|
||||
display:grid;
|
||||
gap:8px;
|
||||
}
|
||||
.ps-list-item{
|
||||
display:grid;
|
||||
grid-template-columns:minmax(0,1fr) 34px;
|
||||
align-items:start;
|
||||
gap:8px;
|
||||
}
|
||||
.ps-field input,
|
||||
.ps-field select,
|
||||
.ps-field textarea,
|
||||
.ps-list-item textarea,
|
||||
.ps-numfield input{
|
||||
width:100%;
|
||||
border:1px solid var(--border-subtle);
|
||||
border-radius:var(--radius-sm);
|
||||
background:var(--bg-surface);
|
||||
color:var(--text-body);
|
||||
font:inherit;
|
||||
}
|
||||
.ps-field input,
|
||||
.ps-field select,
|
||||
.ps-numfield input{
|
||||
min-height:38px;
|
||||
padding:0 10px;
|
||||
}
|
||||
.ps-field textarea,
|
||||
.ps-list-item textarea{
|
||||
min-height:92px;
|
||||
resize:vertical;
|
||||
padding:10px;
|
||||
line-height:1.55;
|
||||
}
|
||||
.ps-list-item textarea{
|
||||
min-height:44px;
|
||||
}
|
||||
.ps-list-item__remove{
|
||||
display:grid;
|
||||
place-items:center;
|
||||
width:34px;
|
||||
height:34px;
|
||||
border:1px solid var(--border-subtle);
|
||||
border-radius:var(--radius-sm);
|
||||
background:var(--bg-surface);
|
||||
color:var(--text-muted);
|
||||
cursor:pointer;
|
||||
}
|
||||
.ps-list-item__remove:hover{
|
||||
color:var(--danger);
|
||||
border-color:var(--danger);
|
||||
}
|
||||
.ps-field input:focus,
|
||||
.ps-field select:focus,
|
||||
.ps-field textarea:focus,
|
||||
.ps-list-item textarea:focus,
|
||||
.ps-numfield input:focus{
|
||||
outline:2px solid var(--accent);
|
||||
outline-offset:1px;
|
||||
border-color:var(--accent);
|
||||
}
|
||||
.ps-prompt{
|
||||
padding:16px;
|
||||
display:grid;
|
||||
gap:12px;
|
||||
}
|
||||
.ps-prompt-section{
|
||||
display:grid;
|
||||
gap:10px;
|
||||
padding:14px;
|
||||
border:1px solid var(--border-subtle);
|
||||
border-radius:var(--radius-sm);
|
||||
background:var(--bg-surface);
|
||||
}
|
||||
.ps-prompt-section__head{
|
||||
display:grid;
|
||||
gap:4px;
|
||||
}
|
||||
.ps-prompt-section__head h3{
|
||||
margin:0;
|
||||
color:var(--text-heading);
|
||||
font-size:var(--fs-md);
|
||||
line-height:1.25;
|
||||
}
|
||||
.ps-prompt-section__head p,
|
||||
.ps-prompt-body,
|
||||
.ps-prompt-empty{
|
||||
margin:0;
|
||||
color:var(--text-muted);
|
||||
font-size:var(--fs-sm);
|
||||
line-height:1.55;
|
||||
}
|
||||
.ps-prompt-rows{
|
||||
display:grid;
|
||||
gap:7px;
|
||||
margin:0;
|
||||
}
|
||||
.ps-prompt-row{
|
||||
display:grid;
|
||||
grid-template-columns:minmax(110px,0.34fr) minmax(0,1fr);
|
||||
gap:10px;
|
||||
align-items:start;
|
||||
padding:8px 0;
|
||||
border-top:1px solid var(--border-faint);
|
||||
}
|
||||
.ps-prompt-row dt{
|
||||
color:var(--text-muted);
|
||||
font-size:var(--fs-xs);
|
||||
font-weight:760;
|
||||
}
|
||||
.ps-prompt-row dd{
|
||||
margin:0;
|
||||
color:var(--text-body);
|
||||
font-size:var(--fs-sm);
|
||||
line-height:1.55;
|
||||
white-space:pre-wrap;
|
||||
overflow-wrap:anywhere;
|
||||
}
|
||||
.ps-prompt-list{
|
||||
display:grid;
|
||||
gap:6px;
|
||||
margin:0;
|
||||
padding-left:18px;
|
||||
color:var(--text-body);
|
||||
font-size:var(--fs-sm);
|
||||
line-height:1.5;
|
||||
}
|
||||
.ps-prompt-list li{
|
||||
overflow-wrap:anywhere;
|
||||
}
|
||||
.ps-actions{
|
||||
display:flex;
|
||||
justify-content:flex-end;
|
||||
gap:8px;
|
||||
}
|
||||
.ps-status{
|
||||
margin:0;
|
||||
padding:10px 12px;
|
||||
border:1px solid var(--accent);
|
||||
border-radius:var(--radius-sm);
|
||||
color:var(--accent-deep);
|
||||
background:var(--accent-tint);
|
||||
font-size:var(--fs-sm);
|
||||
font-weight:700;
|
||||
}
|
||||
.ps-status.is-error{
|
||||
border-color:var(--crit-solid);
|
||||
color:var(--crit-text);
|
||||
background:var(--crit-tint);
|
||||
}
|
||||
.ps-check-row{
|
||||
display:grid;
|
||||
grid-template-columns:12px minmax(0,1fr);
|
||||
gap:8px;
|
||||
align-items:center;
|
||||
min-height:20px;
|
||||
font-size:var(--fs-xs);
|
||||
line-height:1.45;
|
||||
color:var(--text-body);
|
||||
}
|
||||
.ps-check-row .vg-dot{
|
||||
justify-self:center;
|
||||
align-self:center;
|
||||
}
|
||||
.ps-check-row--stack{
|
||||
align-items:start;
|
||||
}
|
||||
.ps-check-row--stack .vg-dot{
|
||||
margin-top:5px;
|
||||
}
|
||||
.ps-check-row--stack span{
|
||||
display:grid;
|
||||
gap:2px;
|
||||
}
|
||||
.ps-check-row--stack b{
|
||||
color:var(--text-strong);
|
||||
font-size:var(--fs-xs);
|
||||
}
|
||||
.ps-check-row--stack small{
|
||||
color:var(--text-muted);
|
||||
font-size:11px;
|
||||
overflow-wrap:anywhere;
|
||||
}
|
||||
.ps-evidence{
|
||||
display:grid;
|
||||
gap:5px;
|
||||
padding:9px;
|
||||
border:1px solid var(--hair);
|
||||
border-radius:var(--radius-sm);
|
||||
background:var(--bg-surface-2);
|
||||
color:var(--text-muted);
|
||||
font-size:11px;
|
||||
line-height:1.45;
|
||||
}
|
||||
.ps-evidence b{
|
||||
color:var(--accent-deep);
|
||||
font-family:var(--font-num);
|
||||
}
|
||||
.ps-review{
|
||||
display:grid;
|
||||
gap:8px;
|
||||
padding:10px;
|
||||
border:1px solid var(--border-subtle);
|
||||
border-radius:var(--radius);
|
||||
background:var(--bg-surface);
|
||||
}
|
||||
.ps-review > div:first-child{
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:space-between;
|
||||
gap:8px;
|
||||
}
|
||||
.ps-review__actions{
|
||||
display:grid;
|
||||
grid-template-columns:repeat(3,minmax(0,1fr));
|
||||
gap:6px;
|
||||
}
|
||||
.ps-review__actions .vg-btn{
|
||||
width:100%;
|
||||
}
|
||||
.ps-empty{
|
||||
display:grid;
|
||||
gap:4px;
|
||||
padding:14px;
|
||||
border:1px dashed var(--border-subtle);
|
||||
border-radius:var(--radius);
|
||||
color:var(--text-muted);
|
||||
background:var(--bg-surface-2);
|
||||
}
|
||||
.ps-empty b{
|
||||
color:var(--text-strong);
|
||||
}
|
||||
@media (max-width:1180px){
|
||||
.ps-layout{
|
||||
grid-template-columns:minmax(210px,260px) minmax(0,1fr);
|
||||
}
|
||||
.ps-inspector{
|
||||
position:static;
|
||||
grid-column:1 / -1;
|
||||
grid-template-columns:repeat(2,minmax(0,1fr));
|
||||
}
|
||||
.ps-source-grid{
|
||||
grid-template-columns:1fr;
|
||||
}
|
||||
.ps-source-panel__actions{
|
||||
display:grid;
|
||||
}
|
||||
}
|
||||
@media (max-width:860px){
|
||||
.ps-head{
|
||||
display:grid;
|
||||
}
|
||||
.ps-head__actions{
|
||||
justify-content:start;
|
||||
}
|
||||
.ps-layout{
|
||||
grid-template-columns:1fr;
|
||||
}
|
||||
.ps-rail{
|
||||
position:static;
|
||||
}
|
||||
.ps-inspector{
|
||||
grid-template-columns:1fr;
|
||||
}
|
||||
.ps-form-grid,
|
||||
.ps-number-grid{
|
||||
grid-template-columns:1fr;
|
||||
}
|
||||
.ps-guidance,
|
||||
.ps-upload-card{
|
||||
grid-template-columns:1fr;
|
||||
}
|
||||
}
|
||||
914
apps/web/src/pages/professor.css
Normal file
914
apps/web/src/pages/professor.css
Normal file
|
|
@ -0,0 +1,914 @@
|
|||
.pf-root{
|
||||
max-width:1280px;
|
||||
margin:0 auto;
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
gap:16px;
|
||||
}
|
||||
.pf-head{
|
||||
display:flex;
|
||||
align-items:flex-end;
|
||||
justify-content:space-between;
|
||||
gap:var(--sp-4);
|
||||
flex-wrap:wrap;
|
||||
}
|
||||
.pf-head__actions{
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:flex-end;
|
||||
gap:8px;
|
||||
flex-wrap:wrap;
|
||||
}
|
||||
.pf-head h1{
|
||||
margin:6px 0 0;
|
||||
color:var(--text-strong);
|
||||
font-size:var(--fs-h2);
|
||||
line-height:1.28;
|
||||
letter-spacing:0;
|
||||
}
|
||||
.pf-head p{
|
||||
margin:6px 0 0;
|
||||
max-width:760px;
|
||||
color:var(--text-body);
|
||||
font-size:var(--fs-xs);
|
||||
line-height:1.55;
|
||||
}
|
||||
.pf-signal-strip{
|
||||
order:1;
|
||||
display:grid;
|
||||
grid-template-columns:1fr;
|
||||
gap:0;
|
||||
min-width:0;
|
||||
}
|
||||
.pf-triage{
|
||||
display:grid;
|
||||
grid-template-columns:auto minmax(0,1fr) auto;
|
||||
align-items:center;
|
||||
gap:12px;
|
||||
min-width:0;
|
||||
padding:14px 16px;
|
||||
border:1px solid var(--hair);
|
||||
border-radius:var(--radius);
|
||||
background:var(--bg-surface);
|
||||
box-shadow:var(--shadow-sm);
|
||||
}
|
||||
.pf-triage.is-active{
|
||||
background:color-mix(in srgb,var(--accent-tint) 42%,var(--bg-surface));
|
||||
}
|
||||
.pf-triage__copy{
|
||||
min-width:0;
|
||||
}
|
||||
.pf-triage__copy .vg-kicker{
|
||||
margin-bottom:4px;
|
||||
}
|
||||
.pf-triage__copy b{
|
||||
display:block;
|
||||
color:var(--text-strong);
|
||||
font-size:var(--fs-body);
|
||||
line-height:1.35;
|
||||
}
|
||||
.pf-triage__copy span,
|
||||
.pf-triage__meta small{
|
||||
color:var(--text-muted);
|
||||
font-size:var(--fs-xs);
|
||||
}
|
||||
.pf-triage__meta{
|
||||
display:grid;
|
||||
grid-template-columns:repeat(2,minmax(0,1fr));
|
||||
gap:0;
|
||||
min-width:180px;
|
||||
border:1px solid var(--border-subtle);
|
||||
border-radius:var(--radius);
|
||||
background:color-mix(in srgb,var(--bg-surface) 86%,transparent);
|
||||
overflow:hidden;
|
||||
}
|
||||
.pf-triage__meta span{
|
||||
display:grid;
|
||||
align-content:center;
|
||||
gap:2px;
|
||||
padding:9px 12px;
|
||||
}
|
||||
.pf-triage__meta span + span{
|
||||
border-left:1px solid var(--hair);
|
||||
}
|
||||
.pf-triage__meta b{
|
||||
color:var(--text-strong);
|
||||
font-family:var(--font-num);
|
||||
font-size:17px;
|
||||
line-height:1;
|
||||
}
|
||||
.pf-error{
|
||||
display:flex;
|
||||
align-items:center;
|
||||
gap:10px;
|
||||
padding:12px 14px;
|
||||
border-radius:var(--radius);
|
||||
background:var(--crit-tint);
|
||||
color:var(--crit-text);
|
||||
font-size:var(--fs-sm);
|
||||
}
|
||||
.pf-kpis{
|
||||
display:grid;
|
||||
grid-template-columns:repeat(6,minmax(0,1fr));
|
||||
border:1px solid var(--hair);
|
||||
border-radius:var(--radius);
|
||||
overflow:hidden;
|
||||
background:var(--bg-surface);
|
||||
box-shadow:var(--shadow-sm);
|
||||
}
|
||||
.pf-kpi{
|
||||
position:relative;
|
||||
min-width:0;
|
||||
padding:14px 16px;
|
||||
display:grid;
|
||||
grid-template-columns:minmax(0,1fr) auto;
|
||||
gap:4px 10px;
|
||||
}
|
||||
.pf-kpi:nth-child(n+2){border-left:1px solid var(--hair);}
|
||||
.pf-kpi:nth-child(n+4){border-top:0;}
|
||||
.pf-kpi--primary,
|
||||
.pf-kpi--warn{
|
||||
background:color-mix(in srgb,var(--accent-tint) 32%,var(--bg-surface));
|
||||
}
|
||||
.pf-kpi--warn{
|
||||
background:color-mix(in srgb,var(--warn-tint) 36%,var(--bg-surface));
|
||||
}
|
||||
.pf-kpi--primary .pf-kpi__ic{
|
||||
color:var(--text-on-accent);
|
||||
background:var(--accent);
|
||||
}
|
||||
.pf-kpi--warn .pf-kpi__ic{
|
||||
color:var(--warn-text);
|
||||
background:var(--warn-tint);
|
||||
}
|
||||
.pf-kpi__ic{
|
||||
grid-column:2;
|
||||
grid-row:1 / span 3;
|
||||
width:30px;
|
||||
height:30px;
|
||||
display:grid;
|
||||
place-items:center;
|
||||
border-radius:var(--radius);
|
||||
color:var(--accent);
|
||||
background:var(--accent-tint);
|
||||
}
|
||||
.pf-kpi__lab{
|
||||
display:block;
|
||||
color:var(--text-muted);
|
||||
font-size:var(--fs-xs);
|
||||
font-weight:700;
|
||||
}
|
||||
.pf-kpi b{
|
||||
display:block;
|
||||
color:var(--text-strong);
|
||||
font-family:var(--font-num);
|
||||
font-size:24px;
|
||||
line-height:1;
|
||||
}
|
||||
.pf-kpi small{
|
||||
color:var(--text-muted);
|
||||
font-size:12px;
|
||||
line-height:1.35;
|
||||
}
|
||||
.pf-workspace{
|
||||
order:2;
|
||||
display:grid;
|
||||
grid-template-columns:minmax(320px,390px) minmax(0,1fr);
|
||||
align-items:start;
|
||||
gap:14px;
|
||||
min-width:0;
|
||||
}
|
||||
.pf-queue-stack{
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
gap:14px;
|
||||
min-width:0;
|
||||
}
|
||||
.pf-section{
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
gap:10px;
|
||||
min-width:0;
|
||||
}
|
||||
.pf-section__head{
|
||||
display:flex;
|
||||
align-items:flex-end;
|
||||
justify-content:space-between;
|
||||
gap:12px;
|
||||
}
|
||||
.pf-section__head h2{
|
||||
margin:4px 0 0;
|
||||
color:var(--text-strong);
|
||||
font-size:var(--fs-body);
|
||||
font-weight:700;
|
||||
line-height:1.3;
|
||||
letter-spacing:0;
|
||||
}
|
||||
.pf-panel{
|
||||
padding:0;
|
||||
overflow:hidden;
|
||||
}
|
||||
.pf-studio-card{
|
||||
display:grid;
|
||||
grid-template-columns:minmax(0,1fr) auto;
|
||||
gap:14px;
|
||||
align-items:center;
|
||||
padding:14px;
|
||||
background:linear-gradient(180deg,var(--bg-surface),color-mix(in srgb,var(--accent-tint) 18%,var(--bg-surface)));
|
||||
}
|
||||
.pf-studio-card__copy{
|
||||
min-width:0;
|
||||
display:grid;
|
||||
gap:7px;
|
||||
}
|
||||
.pf-studio-card__copy b{
|
||||
color:var(--text-strong);
|
||||
font-size:var(--fs-body);
|
||||
}
|
||||
.pf-studio-card__copy p{
|
||||
margin:0;
|
||||
color:var(--text-muted);
|
||||
font-size:var(--fs-xs);
|
||||
line-height:1.55;
|
||||
}
|
||||
.pf-studio-card__meta{
|
||||
display:flex;
|
||||
flex-wrap:wrap;
|
||||
gap:6px;
|
||||
}
|
||||
.pf-studio-card__meta span{
|
||||
padding:4px 7px;
|
||||
border:1px solid var(--hair);
|
||||
border-radius:var(--radius-sm);
|
||||
color:var(--text-body);
|
||||
background:var(--bg-surface-2);
|
||||
font-size:11px;
|
||||
line-height:1.2;
|
||||
}
|
||||
.pf-studio-card__actions{
|
||||
display:flex;
|
||||
justify-content:flex-end;
|
||||
}
|
||||
.pf-section--growth{
|
||||
order:3;
|
||||
min-width:0;
|
||||
}
|
||||
.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;
|
||||
display:grid;
|
||||
grid-template-columns:repeat(3,minmax(0,1fr));
|
||||
gap:12px;
|
||||
padding:12px;
|
||||
}
|
||||
.pf-growth-card{
|
||||
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;
|
||||
}
|
||||
.pf-growth-card__top{
|
||||
display:flex;
|
||||
align-items:flex-start;
|
||||
justify-content:space-between;
|
||||
gap:10px;
|
||||
min-width:0;
|
||||
}
|
||||
.pf-growth-card__id{
|
||||
min-width:0;
|
||||
display:grid;
|
||||
gap:3px;
|
||||
}
|
||||
.pf-growth-card__id b{
|
||||
color:var(--text-strong);
|
||||
font-size:var(--fs-sm);
|
||||
line-height:1.35;
|
||||
overflow:hidden;
|
||||
text-overflow:ellipsis;
|
||||
white-space:nowrap;
|
||||
}
|
||||
.pf-growth-card__id span,
|
||||
.pf-growth-card__metrics small,
|
||||
.pf-growth-point span,
|
||||
.pf-recent__review-state{
|
||||
color:var(--text-muted);
|
||||
font-size:12px;
|
||||
line-height:1.4;
|
||||
}
|
||||
.pf-recent__review-state{
|
||||
display:block;
|
||||
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;
|
||||
height:100%;
|
||||
display:grid;
|
||||
grid-template-rows:minmax(0,1fr) 14px;
|
||||
gap:4px;
|
||||
align-items:end;
|
||||
}
|
||||
.pf-growth-bar i{
|
||||
display:block;
|
||||
width:100%;
|
||||
min-height:6px;
|
||||
border-radius:6px 6px 3px 3px;
|
||||
background:linear-gradient(180deg,var(--accent),var(--accent-deep));
|
||||
}
|
||||
.pf-growth-bar.is-empty i{
|
||||
background:repeating-linear-gradient(135deg,var(--paper-2),var(--paper-2) 3px,var(--hair) 3px,var(--hair) 6px);
|
||||
}
|
||||
.pf-growth-bar small{
|
||||
color:var(--text-muted);
|
||||
font-family:var(--font-num);
|
||||
font-size:10px;
|
||||
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-empty{
|
||||
min-height:118px;
|
||||
display:grid;
|
||||
place-items:center;
|
||||
gap:6px;
|
||||
padding:var(--sp-5);
|
||||
text-align:center;
|
||||
}
|
||||
.pf-empty b{
|
||||
color:var(--text-strong);
|
||||
font-size:var(--fs-body);
|
||||
}
|
||||
.pf-empty span{
|
||||
color:var(--text-muted);
|
||||
font-size:var(--fs-xs);
|
||||
}
|
||||
.pf-list{
|
||||
max-height:min(320px,42vh);
|
||||
overflow:auto;
|
||||
scrollbar-gutter:stable;
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
}
|
||||
.pf-personas{
|
||||
max-height:min(320px,42vh);
|
||||
overflow:auto;
|
||||
scrollbar-gutter:stable;
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
}
|
||||
.pf-persona{
|
||||
display:grid;
|
||||
grid-template-columns:minmax(0,1fr);
|
||||
gap:8px;
|
||||
align-items:start;
|
||||
padding:12px;
|
||||
border-top:1px solid var(--paper-2);
|
||||
}
|
||||
.pf-persona:first-child{border-top:0;}
|
||||
.pf-persona__main{
|
||||
min-width:0;
|
||||
display:grid;
|
||||
grid-template-columns:40px minmax(0,1fr);
|
||||
gap:10px;
|
||||
align-items:center;
|
||||
}
|
||||
.pf-persona__code{
|
||||
width:40px;
|
||||
height:32px;
|
||||
display:grid;
|
||||
place-items:center;
|
||||
border-radius:var(--radius);
|
||||
color:var(--accent-deep);
|
||||
background:var(--accent-tint);
|
||||
font-family:var(--font-num);
|
||||
font-weight:800;
|
||||
font-size:12px;
|
||||
}
|
||||
.pf-persona__main b{
|
||||
display:block;
|
||||
color:var(--text-strong);
|
||||
font-size:var(--fs-sm);
|
||||
line-height:1.35;
|
||||
overflow:hidden;
|
||||
text-overflow:ellipsis;
|
||||
white-space:nowrap;
|
||||
}
|
||||
.pf-persona__main span,
|
||||
.pf-persona p,
|
||||
.pf-persona__meta span{
|
||||
color:var(--text-muted);
|
||||
font-size:var(--fs-xs);
|
||||
line-height:1.45;
|
||||
}
|
||||
.pf-persona p{
|
||||
margin:0;
|
||||
min-width:0;
|
||||
overflow:hidden;
|
||||
text-overflow:ellipsis;
|
||||
white-space:nowrap;
|
||||
}
|
||||
.pf-persona__meta{
|
||||
display:flex;
|
||||
align-items:center;
|
||||
gap:10px;
|
||||
justify-content:space-between;
|
||||
white-space:nowrap;
|
||||
}
|
||||
.pf-persona__actions{
|
||||
display:flex;
|
||||
justify-content:flex-end;
|
||||
gap:8px;
|
||||
min-width:0;
|
||||
}
|
||||
.pf-persona__actions .vg-btn{
|
||||
min-width:72px;
|
||||
padding-inline:10px;
|
||||
}
|
||||
.pf-alerts{
|
||||
max-height:min(300px,38vh);
|
||||
overflow:auto;
|
||||
scrollbar-gutter:stable;
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
}
|
||||
.pf-alert{
|
||||
display:grid;
|
||||
grid-template-columns:auto minmax(0,1fr) auto;
|
||||
gap:10px;
|
||||
align-items:center;
|
||||
padding:12px;
|
||||
border-top:1px solid var(--paper-2);
|
||||
background:color-mix(in srgb,var(--warn-tint) 34%,transparent);
|
||||
}
|
||||
.pf-alert:first-child{border-top:0;}
|
||||
.pf-alert__ic{
|
||||
width:30px;
|
||||
height:30px;
|
||||
display:grid;
|
||||
place-items:center;
|
||||
border-radius:var(--radius);
|
||||
color:var(--warn-text);
|
||||
background:var(--warn-tint);
|
||||
}
|
||||
.pf-alert__main{
|
||||
min-width:0;
|
||||
display:grid;
|
||||
gap:3px;
|
||||
}
|
||||
.pf-alert__main b{
|
||||
color:var(--text-strong);
|
||||
font-size:var(--fs-sm);
|
||||
}
|
||||
.pf-alert__main span,
|
||||
.pf-alert__main code,
|
||||
.pf-alert__resource span{
|
||||
color:var(--text-muted);
|
||||
font-size:12px;
|
||||
}
|
||||
.pf-alert__main code{
|
||||
overflow:hidden;
|
||||
text-overflow:ellipsis;
|
||||
white-space:nowrap;
|
||||
}
|
||||
.pf-alert__resource{
|
||||
display:grid;
|
||||
gap:2px;
|
||||
justify-items:end;
|
||||
min-width:86px;
|
||||
}
|
||||
.pf-alert__resource b{
|
||||
color:var(--warn-text);
|
||||
font-family:var(--font-num);
|
||||
font-size:18px;
|
||||
}
|
||||
.pf-session{
|
||||
width:100%;
|
||||
font:inherit;
|
||||
text-align:left;
|
||||
background:transparent;
|
||||
color:inherit;
|
||||
display:grid;
|
||||
grid-template-columns:8px minmax(0,1fr) auto;
|
||||
gap:8px 10px;
|
||||
align-items:start;
|
||||
padding:12px;
|
||||
border:0;
|
||||
border-top:1px solid var(--paper-2);
|
||||
cursor:default;
|
||||
}
|
||||
.pf-session:first-child{border-top:0;}
|
||||
.pf-session--action{
|
||||
cursor:pointer;
|
||||
}
|
||||
.pf-session--action:hover{
|
||||
background:var(--bg-surface-2);
|
||||
}
|
||||
.pf-session--action:focus-visible{
|
||||
outline:2px solid var(--accent);
|
||||
outline-offset:-2px;
|
||||
}
|
||||
.pf-session__dot{
|
||||
width:8px;
|
||||
height:8px;
|
||||
border-radius:50%;
|
||||
background:var(--accent);
|
||||
}
|
||||
.pf-session__main{
|
||||
min-width:0;
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
gap:3px;
|
||||
}
|
||||
.pf-session__main b{
|
||||
color:var(--text-strong);
|
||||
font-size:var(--fs-sm);
|
||||
}
|
||||
.pf-session__main span,.pf-session__main code{
|
||||
color:var(--text-muted);
|
||||
font-size:var(--fs-xs);
|
||||
overflow-wrap:anywhere;
|
||||
}
|
||||
.pf-session__main code,.pf-recent__learner code{
|
||||
font-family:var(--font-num);
|
||||
overflow:hidden;
|
||||
text-overflow:ellipsis;
|
||||
}
|
||||
.pf-session__meta{
|
||||
grid-column:2 / 4;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:space-between;
|
||||
gap:8px;
|
||||
color:var(--text-muted);
|
||||
font-family:var(--font-num);
|
||||
font-size:var(--fs-xs);
|
||||
white-space:normal;
|
||||
}
|
||||
.pf-session__open{
|
||||
grid-column:3;
|
||||
grid-row:1;
|
||||
display:inline-flex;
|
||||
align-items:center;
|
||||
gap:5px;
|
||||
align-self:center;
|
||||
justify-self:end;
|
||||
min-height:30px;
|
||||
padding:0 9px;
|
||||
border:1px solid var(--hair);
|
||||
border-radius:var(--radius-sm);
|
||||
color:var(--accent-deep);
|
||||
background:var(--accent-tint);
|
||||
font-size:var(--fs-xs);
|
||||
font-weight:760;
|
||||
white-space:nowrap;
|
||||
}
|
||||
.pf-session--action:hover .pf-session__open{
|
||||
border-color:var(--accent);
|
||||
}
|
||||
.pf-recent-list{
|
||||
max-height:min(620px,calc(100vh - 220px));
|
||||
overflow:auto;
|
||||
scrollbar-gutter:stable;
|
||||
min-width:0;
|
||||
}
|
||||
.pf-recent-head,
|
||||
.pf-recent-row{
|
||||
display:grid;
|
||||
grid-template-columns:
|
||||
minmax(128px,1.25fr)
|
||||
minmax(58px,.55fr)
|
||||
minmax(66px,.55fr)
|
||||
minmax(82px,.85fr)
|
||||
minmax(32px,.35fr)
|
||||
minmax(70px,.6fr)
|
||||
minmax(66px,.55fr)
|
||||
minmax(86px,.65fr);
|
||||
align-items:center;
|
||||
gap:8px;
|
||||
min-width:0;
|
||||
}
|
||||
.pf-recent-head{
|
||||
position:sticky;
|
||||
top:0;
|
||||
z-index:1;
|
||||
color:var(--text-muted);
|
||||
font-size:var(--fs-xs);
|
||||
font-weight:700;
|
||||
padding:9px 12px;
|
||||
border-bottom:1px solid var(--hair);
|
||||
background:var(--bg-surface);
|
||||
white-space:nowrap;
|
||||
}
|
||||
.pf-recent-row{
|
||||
width:100%;
|
||||
font:inherit;
|
||||
text-align:left;
|
||||
color:inherit;
|
||||
background:transparent;
|
||||
padding:10px 12px;
|
||||
border:0;
|
||||
border-top:1px solid var(--paper-2);
|
||||
}
|
||||
.pf-recent-row--action{
|
||||
cursor:pointer;
|
||||
}
|
||||
.pf-recent-row--action:hover{
|
||||
background:var(--bg-surface-2);
|
||||
}
|
||||
.pf-recent-row--action:focus-visible{
|
||||
outline:2px solid var(--accent);
|
||||
outline-offset:-2px;
|
||||
}
|
||||
.pf-recent-head + .pf-recent-row{
|
||||
border-top:0;
|
||||
}
|
||||
.pf-recent__learner,
|
||||
.pf-recent__cell{
|
||||
min-width:0;
|
||||
color:var(--text-body);
|
||||
font-size:var(--fs-sm);
|
||||
overflow-wrap:anywhere;
|
||||
}
|
||||
.pf-recent__cell::before{
|
||||
display:none;
|
||||
}
|
||||
.pf-recent__learner{
|
||||
min-width:0;
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
gap:3px;
|
||||
}
|
||||
.pf-recent__learner b{
|
||||
color:var(--text-strong);
|
||||
font-size:var(--fs-sm);
|
||||
line-height:1.35;
|
||||
overflow-wrap:anywhere;
|
||||
}
|
||||
.pf-recent__learner code{
|
||||
max-width:100%;
|
||||
color:var(--text-muted);
|
||||
font-size:11px;
|
||||
white-space:nowrap;
|
||||
overflow:hidden;
|
||||
text-overflow:ellipsis;
|
||||
}
|
||||
.pf-recent__cell .vg-badge{
|
||||
justify-self:start;
|
||||
}
|
||||
.pf-recent__cell--open{
|
||||
display:flex;
|
||||
justify-content:flex-end;
|
||||
}
|
||||
.pf-recent__open{
|
||||
display:inline-flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
gap:4px;
|
||||
min-height:30px;
|
||||
padding:0 7px;
|
||||
border:1px solid var(--hair);
|
||||
border-radius:var(--radius-sm);
|
||||
color:var(--accent-deep);
|
||||
background:var(--accent-tint);
|
||||
font-size:var(--fs-xs);
|
||||
font-weight:760;
|
||||
white-space:nowrap;
|
||||
}
|
||||
.pf-recent-row--action:hover .pf-recent__open{
|
||||
border-color:var(--accent);
|
||||
}
|
||||
@media (max-width:1100px){
|
||||
.pf-signal-strip,
|
||||
.pf-workspace{
|
||||
grid-template-columns:1fr;
|
||||
}
|
||||
.pf-triage{
|
||||
grid-template-columns:auto minmax(0,max-content) auto;
|
||||
justify-content:start;
|
||||
}
|
||||
.pf-kpis{
|
||||
grid-template-columns:repeat(4,minmax(0,1fr));
|
||||
}
|
||||
.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-list,
|
||||
.pf-personas{
|
||||
max-height:360px;
|
||||
}
|
||||
.pf-recent-list{
|
||||
max-height:460px;
|
||||
}
|
||||
}
|
||||
@media (max-width:860px){
|
||||
.pf-head__actions{
|
||||
width:100%;
|
||||
justify-content:flex-start;
|
||||
}
|
||||
.pf-triage{
|
||||
grid-template-columns:auto minmax(0,1fr);
|
||||
}
|
||||
.pf-triage__meta{
|
||||
grid-column:1 / -1;
|
||||
width:100%;
|
||||
}
|
||||
.pf-kpis{grid-template-columns:repeat(2,minmax(0,1fr));}
|
||||
.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-recent-list{
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
gap:10px;
|
||||
padding:10px;
|
||||
background:var(--bg-surface-2);
|
||||
overflow-x:hidden;
|
||||
}
|
||||
.pf-recent-head{
|
||||
display:none;
|
||||
}
|
||||
.pf-recent-row{
|
||||
display:grid;
|
||||
grid-template-columns:repeat(2,minmax(0,1fr));
|
||||
gap:10px 12px;
|
||||
padding:12px;
|
||||
border:1px solid var(--paper-2);
|
||||
border-radius:var(--radius);
|
||||
background:var(--bg-surface);
|
||||
}
|
||||
.pf-recent__learner{
|
||||
grid-column:1 / -1;
|
||||
padding-bottom:10px;
|
||||
border-bottom:1px solid var(--paper-2);
|
||||
}
|
||||
.pf-recent__learner code{
|
||||
white-space:normal;
|
||||
overflow-wrap:anywhere;
|
||||
}
|
||||
.pf-recent__cell{
|
||||
display:grid;
|
||||
grid-template-columns:minmax(74px,.4fr) minmax(0,1fr);
|
||||
gap:8px;
|
||||
align-items:center;
|
||||
}
|
||||
.pf-recent__cell--open{
|
||||
justify-content:stretch;
|
||||
}
|
||||
.pf-recent__cell::before{
|
||||
display:block;
|
||||
content:attr(data-label);
|
||||
color:var(--text-muted);
|
||||
font-size:var(--fs-xs);
|
||||
font-weight:700;
|
||||
line-height:1.35;
|
||||
}
|
||||
.pf-session{
|
||||
grid-template-columns:8px minmax(0,1fr);
|
||||
}
|
||||
.pf-session__meta{
|
||||
grid-column:2;
|
||||
justify-content:flex-start;
|
||||
}
|
||||
.pf-session__open{
|
||||
grid-column:2;
|
||||
grid-row:auto;
|
||||
justify-self:start;
|
||||
}
|
||||
}
|
||||
@media (max-width:520px){
|
||||
.pf-kpis{grid-template-columns:repeat(2,minmax(0,1fr));}
|
||||
.pf-kpi,
|
||||
.pf-kpi + .pf-kpi{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-persona__actions{
|
||||
display:grid;
|
||||
grid-template-columns:repeat(2,minmax(0,1fr));
|
||||
}
|
||||
.pf-persona__actions .vg-btn{
|
||||
width:100%;
|
||||
}
|
||||
.pf-growth-list{
|
||||
padding:10px;
|
||||
}
|
||||
.pf-growth-card__metrics{
|
||||
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);
|
||||
}
|
||||
.pf-alert__resource{
|
||||
grid-column:2;
|
||||
justify-items:start;
|
||||
}
|
||||
.pf-recent-list{
|
||||
padding:8px;
|
||||
}
|
||||
.pf-recent-row{
|
||||
grid-template-columns:1fr;
|
||||
gap:10px;
|
||||
}
|
||||
.pf-recent__cell{
|
||||
grid-template-columns:minmax(64px,.32fr) minmax(0,1fr);
|
||||
}
|
||||
.pf-recent__cell--open .pf-recent__open{
|
||||
justify-self:start;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue