refactor(design-tokens): 全量体系化重建设计令牌
Primitive + Semantic 双层令牌架构,HEX->HSL,明暗双份,@theme inline 暴露为 Tailwind 类。 - 新建 src/app/styles/tokens/ 6 个令牌文件(primitive/semantic-light/semantic-dark/lesson-preparation/tailwind-theme/index) - globals.css 改为 @import 引入,477->258 行 - 清理 91 处 #hex 硬编码颜色 -> hsl(var(--*)) - 清理 10 处硬编码字体 -> var(--font-family-*) - 清理 100 文件 Tailwind 任意值(Tier 1 映射/Tier 3 注释豁免) - 清理 M3 Surface 死代码,升级 --lp-* 令牌(HEX->HSL + 暗色补全) - 新建 ESLint 自定义规则 no-hardcoded-design-tokens(单词边界正则) - eslint.config.mjs 新增 no-restricted-syntax 禁止 #hex + 自定义规则加载(pathToFileURL) - 项目规则新增设计令牌规范强制章节 - 架构图 004/005 同步设计令牌体系节点 - known-issues.md 追加设计令牌问题分类(7 个规则表) 验证: tsc --noEmit 0 errors, npm run lint 0 errors/12 warnings(均为既有问题)
This commit is contained in:
@@ -4,15 +4,19 @@ export default function CurriculumMapLoading() {
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<div className="space-y-2">
|
||||
{/* arbitrary-value: skeleton placeholder fixed size */}
|
||||
<Skeleton className="h-8 w-[200px]" />
|
||||
{/* arbitrary-value: skeleton placeholder fixed size */}
|
||||
<Skeleton className="h-4 w-[300px]" />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-3">
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
// arbitrary-value: skeleton placeholder fixed size
|
||||
<Skeleton key={i} className="h-[80px] w-full rounded-lg" />
|
||||
))}
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{/* arbitrary-value: skeleton placeholder fixed size */}
|
||||
<Skeleton className="h-[400px] w-full rounded-lg" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -82,6 +82,7 @@ export default async function AdminLessonPlansPage(): Promise<JSX.Element> {
|
||||
fallback={
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
// arbitrary-value: skeleton placeholder fixed size
|
||||
<Skeleton key={i} className="h-[180px] w-full" />
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -26,6 +26,7 @@ export default function GradePracticeError({
|
||||
</h1>
|
||||
<p className="text-muted-foreground">{t("errors.pageErrorGrade")}</p>
|
||||
</div>
|
||||
{/* arbitrary-value: error icon fixed size */}
|
||||
<EmptyState
|
||||
icon={BarChart3}
|
||||
title={t("errors.loadFailed")}
|
||||
|
||||
@@ -4,11 +4,13 @@ export default function ParentCoursePlanDetailLoading() {
|
||||
return (
|
||||
<div className="flex h-full flex-col space-y-6 p-8">
|
||||
<div className="flex items-center justify-between">
|
||||
{/* arbitrary-value: skeleton placeholder fixed size */}
|
||||
<Skeleton className="h-8 w-[240px]" />
|
||||
</div>
|
||||
<Skeleton className="h-4 w-full max-w-md" />
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
{Array.from({ length: 3 }).map((_, i) => (
|
||||
// arbitrary-value: skeleton placeholder fixed size
|
||||
<Skeleton key={i} className="h-[100px] w-full" />
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -4,11 +4,14 @@ export default function ParentCoursePlansLoading() {
|
||||
return (
|
||||
<div className="flex h-full flex-col space-y-8 p-8">
|
||||
<div className="space-y-2">
|
||||
{/* arbitrary-value: skeleton placeholder fixed size */}
|
||||
<Skeleton className="h-8 w-[180px]" />
|
||||
{/* arbitrary-value: skeleton placeholder fixed size */}
|
||||
<Skeleton className="h-4 w-[300px]" />
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
// arbitrary-value: skeleton placeholder fixed size
|
||||
<Skeleton key={i} className="h-[160px] w-full" />
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -34,6 +34,7 @@ export default async function ParentLessonPlansPage(): Promise<JSX.Element> {
|
||||
fallback={
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
// arbitrary-value: skeleton placeholder fixed size
|
||||
<Skeleton key={i} className="h-[180px] w-full" />
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -35,6 +35,7 @@ export default async function ParentPracticePage(): Promise<JSX.Element> {
|
||||
<h1 className="text-2xl font-bold tracking-tight">{t("parent.title")}</h1>
|
||||
<p className="text-muted-foreground">{t("parent.description")}</p>
|
||||
</div>
|
||||
{/* arbitrary-value: empty state fixed size */}
|
||||
<EmptyState
|
||||
icon={Users}
|
||||
title={t("parent.noChild")}
|
||||
|
||||
@@ -4,11 +4,13 @@ export default function StudentCoursePlanDetailLoading() {
|
||||
return (
|
||||
<div className="flex h-full flex-col space-y-6 p-8">
|
||||
<div className="flex items-center justify-between">
|
||||
{/* arbitrary-value: skeleton placeholder fixed size */}
|
||||
<Skeleton className="h-8 w-[240px]" />
|
||||
</div>
|
||||
<Skeleton className="h-4 w-full max-w-md" />
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
{Array.from({ length: 3 }).map((_, i) => (
|
||||
// arbitrary-value: skeleton placeholder fixed size
|
||||
<Skeleton key={i} className="h-[100px] w-full" />
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -4,11 +4,14 @@ export default function StudentCoursePlansLoading() {
|
||||
return (
|
||||
<div className="flex h-full flex-col space-y-8 p-8">
|
||||
<div className="space-y-2">
|
||||
{/* arbitrary-value: skeleton placeholder fixed size */}
|
||||
<Skeleton className="h-8 w-[180px]" />
|
||||
{/* arbitrary-value: skeleton placeholder fixed size */}
|
||||
<Skeleton className="h-4 w-[300px]" />
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
// arbitrary-value: skeleton placeholder fixed size
|
||||
<Skeleton key={i} className="h-[160px] w-full" />
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -34,6 +34,7 @@ export default async function StudentLessonPlansPage(): Promise<JSX.Element> {
|
||||
fallback={
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
// arbitrary-value: skeleton placeholder fixed size
|
||||
<Skeleton key={i} className="h-[180px] w-full" />
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -38,6 +38,7 @@ async function ScheduleResults({ searchParams }: { searchParams: Promise<SearchP
|
||||
|
||||
if (schedule.length === 0) {
|
||||
return (
|
||||
// arbitrary-value: empty state fixed size
|
||||
<EmptyState
|
||||
icon={Calendar}
|
||||
title={hasFilters ? t("schedule.empty.noMatch") : t("schedule.empty.title")}
|
||||
|
||||
@@ -57,6 +57,7 @@ async function StudentsResults({ searchParams, defaultClassId }: { searchParams:
|
||||
|
||||
if (filteredStudents.length === 0) {
|
||||
return (
|
||||
// arbitrary-value: empty state fixed size
|
||||
<EmptyState
|
||||
icon={User}
|
||||
title={hasFilters ? t("students.empty.noMatch") : t("students.empty.title")}
|
||||
|
||||
@@ -5,24 +5,31 @@ export default function CalendarLoading() {
|
||||
<div className="p-6 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-2">
|
||||
{/* arbitrary-value: skeleton placeholder fixed size */}
|
||||
<Skeleton className="h-8 w-[180px]" />
|
||||
{/* arbitrary-value: skeleton placeholder fixed size */}
|
||||
<Skeleton className="h-4 w-[260px]" />
|
||||
</div>
|
||||
{/* arbitrary-value: skeleton placeholder fixed size */}
|
||||
<Skeleton className="h-9 w-[120px]" />
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Skeleton className="h-9 w-9" />
|
||||
<Skeleton className="h-9 w-9" />
|
||||
{/* arbitrary-value: skeleton placeholder fixed size */}
|
||||
<Skeleton className="h-9 w-[80px]" />
|
||||
{/* arbitrary-value: skeleton placeholder fixed size */}
|
||||
<Skeleton className="h-4 w-[200px] ml-2" />
|
||||
</div>
|
||||
{/* arbitrary-value: skeleton placeholder fixed size */}
|
||||
<Skeleton className="h-9 w-[160px]" />
|
||||
</div>
|
||||
<div className="grid grid-cols-7 gap-2">
|
||||
{Array.from({ length: 7 }).map((_, i) => (
|
||||
<div key={i} className="flex flex-col">
|
||||
<Skeleton className="h-6 w-full" />
|
||||
{/* arbitrary-value: skeleton placeholder fixed size */}
|
||||
<div className="flex-1 min-h-[200px] space-y-1 pt-1">
|
||||
<Skeleton className="h-8 w-full" />
|
||||
<Skeleton className="h-8 w-full" />
|
||||
|
||||
@@ -4,16 +4,20 @@ export default function HeatmapLoading() {
|
||||
return (
|
||||
<div className="p-6 space-y-4">
|
||||
<div className="space-y-2">
|
||||
{/* arbitrary-value: skeleton placeholder fixed size */}
|
||||
<Skeleton className="h-8 w-[200px]" />
|
||||
{/* arbitrary-value: skeleton placeholder fixed size */}
|
||||
<Skeleton className="h-4 w-[320px]" />
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{Array.from({ length: 3 }).map((_, i) => (
|
||||
// arbitrary-value: skeleton placeholder fixed size
|
||||
<Skeleton key={i} className="h-[80px] w-full" />
|
||||
))}
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
// arbitrary-value: skeleton placeholder fixed size
|
||||
<Skeleton key={i} className="h-[200px] w-full" />
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -4,11 +4,14 @@ export default function LibraryLoading() {
|
||||
return (
|
||||
<div className="p-6 space-y-4">
|
||||
<div className="space-y-2">
|
||||
{/* arbitrary-value: skeleton placeholder fixed size */}
|
||||
<Skeleton className="h-8 w-[200px]" />
|
||||
{/* arbitrary-value: skeleton placeholder fixed size */}
|
||||
<Skeleton className="h-4 w-[320px]" />
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
// arbitrary-value: skeleton placeholder fixed size
|
||||
<Skeleton key={i} className="h-[160px] w-full" />
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -77,6 +77,7 @@ export default async function LibraryPage(): Promise<JSX.Element> {
|
||||
</span>
|
||||
)}
|
||||
{item.textbookTitle && (
|
||||
// arbitrary-value: truncate max width fixed size
|
||||
<span className="px-1.5 py-0.5 rounded bg-surface-container-highest truncate max-w-[120px]">
|
||||
{item.textbookTitle}
|
||||
</span>
|
||||
|
||||
@@ -34,6 +34,7 @@ export default async function NewLessonPlanPage(): Promise<JSX.Element> {
|
||||
<Skeleton className="h-10 w-full" />
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
// arbitrary-value: skeleton placeholder fixed size
|
||||
<Skeleton key={i} className="h-[100px] w-full" />
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -45,12 +45,16 @@ export default async function LessonPlansPage(): Promise<JSX.Element> {
|
||||
fallback={
|
||||
<div className="space-y-4">
|
||||
<div className="flex gap-2 flex-wrap items-center">
|
||||
{/* arbitrary-value: skeleton placeholder fixed size */}
|
||||
<Skeleton className="h-9 w-[240px]" />
|
||||
{/* arbitrary-value: skeleton placeholder fixed size */}
|
||||
<Skeleton className="h-9 w-[160px]" />
|
||||
{/* arbitrary-value: skeleton placeholder fixed size */}
|
||||
<Skeleton className="h-9 w-[160px]" />
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
// arbitrary-value: skeleton placeholder fixed size
|
||||
<Skeleton key={i} className="h-[180px] w-full" />
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -26,6 +26,7 @@ export default function TeacherPracticeError({
|
||||
</h1>
|
||||
<p className="text-muted-foreground">{t("errors.pageErrorPractice")}</p>
|
||||
</div>
|
||||
{/* arbitrary-value: error icon fixed size */}
|
||||
<EmptyState
|
||||
icon={BarChart3}
|
||||
title={t("errors.loadFailed")}
|
||||
|
||||
@@ -59,6 +59,7 @@ async function TeacherPracticeContent({
|
||||
<h1 className="text-2xl font-bold tracking-tight">{t("teacher.title")}</h1>
|
||||
<p className="text-muted-foreground">{t("teacher.description")}</p>
|
||||
</div>
|
||||
{/* arbitrary-value: empty state fixed size */}
|
||||
<EmptyState
|
||||
icon={BarChart3}
|
||||
title={t("teacher.noClass")}
|
||||
@@ -86,6 +87,7 @@ async function TeacherPracticeContent({
|
||||
<h1 className="text-2xl font-bold tracking-tight">{t("teacher.title")}</h1>
|
||||
<p className="text-muted-foreground">{t("teacher.description")}</p>
|
||||
</div>
|
||||
{/* arbitrary-value: empty state fixed size */}
|
||||
<EmptyState
|
||||
icon={BarChart3}
|
||||
title={t("teacher.noStudent")}
|
||||
@@ -150,6 +152,7 @@ async function TeacherPracticeContent({
|
||||
<h1 className="text-2xl font-bold tracking-tight">{t("teacher.title")}</h1>
|
||||
<p className="text-muted-foreground">{t("teacher.description")}</p>
|
||||
</div>
|
||||
{/* arbitrary-value: empty state fixed size */}
|
||||
<EmptyState
|
||||
icon={BarChart3}
|
||||
title={t("teacher.noData")}
|
||||
@@ -257,9 +260,11 @@ export default async function TeacherPracticePage({
|
||||
<Skeleton className="h-10 w-full" />
|
||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-5">
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
// arbitrary-value: skeleton placeholder fixed size
|
||||
<Skeleton key={i} className="h-[100px]" />
|
||||
))}
|
||||
</div>
|
||||
{/* arbitrary-value: skeleton placeholder fixed size */}
|
||||
<Skeleton className="h-[300px] w-full" />
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,219 +1,9 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@import "./styles/tokens/index.css";
|
||||
@plugin "tailwindcss-animate";
|
||||
@plugin "@tailwindcss/typography";
|
||||
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
|
||||
:root {
|
||||
/* Neutral: Zinc - Clean, Professional, International Style */
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 240 10% 3.9%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 240 10% 3.9%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 240 10% 3.9%;
|
||||
|
||||
/* Brand: Deep Indigo */
|
||||
--primary: 240 5.9% 10%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
|
||||
--secondary: 240 4.8% 95.9%;
|
||||
--secondary-foreground: 240 5.9% 10%;
|
||||
|
||||
--muted: 240 4.8% 95.9%;
|
||||
--muted-foreground: 240 3.8% 46.1%;
|
||||
|
||||
--accent: 240 4.8% 95.9%;
|
||||
--accent-foreground: 240 5.9% 10%;
|
||||
|
||||
/* Destructive: Subtle Red */
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
|
||||
/* Borders & UI */
|
||||
--border: 240 5.9% 90%;
|
||||
--input: 240 5.9% 90%;
|
||||
--ring: 240 5.9% 10%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
|
||||
/* Chart / Data Visualization Colors */
|
||||
--chart-1: 12 76% 61%;
|
||||
--chart-2: 173 58% 39%;
|
||||
--chart-3: 197 37% 24%;
|
||||
--chart-4: 43 74% 66%;
|
||||
--chart-5: 27 87% 67%;
|
||||
|
||||
/* Lesson Plan Node Colors (V4 P1-4 修复:从硬编码提取到设计令牌) */
|
||||
/* V4 纸感重构:原 --lesson-node-* 已删除,改为 --lp-dot-* 中性色点令牌 */
|
||||
|
||||
/* V4 备课编辑器:纸感令牌(替代 --lesson-node-*) */
|
||||
--lp-paper: #fefefe;
|
||||
--lp-paper-edge: #f8f8f7;
|
||||
--lp-paper-shadow: 0 1px 2px rgba(15,15,15,0.04), 0 8px 24px rgba(15,15,15,0.04);
|
||||
--lp-paper-shadow-active: 0 1px 2px rgba(15,15,15,0.06), 0 12px 36px rgba(15,15,15,0.08);
|
||||
--lp-anchor-range: rgba(28, 25, 23, 0.08);
|
||||
--lp-anchor-range-active: rgba(28, 25, 23, 0.16);
|
||||
--lp-anchor-point: #1c1917;
|
||||
--lp-inline-node-border: #d6d3d1;
|
||||
--lp-inline-node-text: #44403c;
|
||||
--lp-inline-node-meta: #a8a29e;
|
||||
--lp-interaction: #6366f1;
|
||||
|
||||
/* V4 节点类型色点(极克制) */
|
||||
--lp-dot-objective: #4b5563;
|
||||
--lp-dot-key-point: #525252;
|
||||
--lp-dot-import: #6b7280;
|
||||
--lp-dot-new-teaching: #1c1917;
|
||||
--lp-dot-consolidation: #6b7280;
|
||||
--lp-dot-summary: #525252;
|
||||
--lp-dot-homework: #525252;
|
||||
--lp-dot-blackboard: #44403c;
|
||||
--lp-dot-text-study: #4b5563;
|
||||
--lp-dot-exercise: #6b7280;
|
||||
--lp-dot-rich-text: #a8a29e;
|
||||
--lp-dot-reflection: #6b7280;
|
||||
--lp-dot-interaction: #6366f1;
|
||||
--lp-dot-textbook: #44403c;
|
||||
--lp-dot-default: #a8a29e;
|
||||
|
||||
/* Sidebar Specific */
|
||||
--sidebar-background: 0 0% 98%;
|
||||
--sidebar-foreground: 240 5.3% 26.1%;
|
||||
--sidebar-primary: 240 5.9% 10%;
|
||||
--sidebar-primary-foreground: 0 0% 98%;
|
||||
--sidebar-accent: 240 4.8% 95.9%;
|
||||
--sidebar-accent-foreground: 240 5.9% 10%;
|
||||
--sidebar-border: 220 13% 91%;
|
||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||
}
|
||||
|
||||
.dark {
|
||||
/* Dark Mode: Deep Zinc Base */
|
||||
--background: 240 10% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
|
||||
--card: 240 10% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
|
||||
--popover: 240 10% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
|
||||
/* Brand Dark: Adjusted for contrast */
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 240 5.9% 10%;
|
||||
|
||||
--secondary: 240 3.7% 15.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
|
||||
--muted: 240 3.7% 15.9%;
|
||||
--muted-foreground: 240 5% 64.9%;
|
||||
|
||||
--accent: 240 3.7% 15.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
|
||||
--border: 240 3.7% 15.9%;
|
||||
--input: 240 3.7% 15.9%;
|
||||
--ring: 240 4.9% 83.9%;
|
||||
|
||||
--chart-1: 220 70% 50%;
|
||||
--chart-2: 160 60% 45%;
|
||||
--chart-3: 30 80% 55%;
|
||||
--chart-4: 280 65% 60%;
|
||||
--chart-5: 340 75% 55%;
|
||||
|
||||
--sidebar-background: 240 5.9% 10%;
|
||||
--sidebar-foreground: 240 4.8% 95.9%;
|
||||
--sidebar-primary: 224.3 76.3% 48%;
|
||||
--sidebar-primary-foreground: 0 0% 100%;
|
||||
--sidebar-accent: 240 3.7% 15.9%;
|
||||
--sidebar-accent-foreground: 240 4.8% 95.9%;
|
||||
--sidebar-border: 240 3.7% 15.9%;
|
||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: hsl(var(--background));
|
||||
--color-foreground: hsl(var(--foreground));
|
||||
|
||||
--color-card: hsl(var(--card));
|
||||
--color-card-foreground: hsl(var(--card-foreground));
|
||||
|
||||
--color-popover: hsl(var(--popover));
|
||||
--color-popover-foreground: hsl(var(--popover-foreground));
|
||||
|
||||
--color-primary: hsl(var(--primary));
|
||||
--color-primary-foreground: hsl(var(--primary-foreground));
|
||||
|
||||
--color-secondary: hsl(var(--secondary));
|
||||
--color-secondary-foreground: hsl(var(--secondary-foreground));
|
||||
|
||||
--color-muted: hsl(var(--muted));
|
||||
--color-muted-foreground: hsl(var(--muted-foreground));
|
||||
|
||||
--color-accent: hsl(var(--accent));
|
||||
--color-accent-foreground: hsl(var(--accent-foreground));
|
||||
|
||||
--color-destructive: hsl(var(--destructive));
|
||||
--color-destructive-foreground: hsl(var(--destructive-foreground));
|
||||
|
||||
--color-border: hsl(var(--border));
|
||||
--color-input: hsl(var(--input));
|
||||
--color-ring: hsl(var(--ring));
|
||||
|
||||
--color-chart-1: hsl(var(--chart-1));
|
||||
--color-chart-2: hsl(var(--chart-2));
|
||||
--color-chart-3: hsl(var(--chart-3));
|
||||
--color-chart-4: hsl(var(--chart-4));
|
||||
--color-chart-5: hsl(var(--chart-5));
|
||||
|
||||
--color-sidebar: hsl(var(--sidebar-background));
|
||||
--color-sidebar-foreground: hsl(var(--sidebar-foreground));
|
||||
--color-sidebar-primary: hsl(var(--sidebar-primary));
|
||||
--color-sidebar-primary-foreground: hsl(var(--sidebar-primary-foreground));
|
||||
--color-sidebar-accent: hsl(var(--sidebar-accent));
|
||||
--color-sidebar-accent-foreground: hsl(var(--sidebar-accent-foreground));
|
||||
--color-sidebar-border: hsl(var(--sidebar-border));
|
||||
--color-sidebar-ring: hsl(var(--sidebar-ring));
|
||||
|
||||
/* Material Design 3 Surface 令牌映射(备课模块使用)*/
|
||||
--color-surface: hsl(var(--card));
|
||||
--color-on-surface: hsl(var(--foreground));
|
||||
--color-on-surface-variant: hsl(var(--muted-foreground));
|
||||
--color-surface-container-lowest: hsl(var(--background));
|
||||
--color-surface-container-low: hsl(var(--muted));
|
||||
--color-surface-container: hsl(var(--secondary));
|
||||
--color-surface-container-high: hsl(var(--accent));
|
||||
--color-surface-container-highest: hsl(var(--muted-foreground));
|
||||
--color-outline-variant: hsl(var(--border));
|
||||
--color-outline: hsl(var(--border));
|
||||
--color-error: hsl(var(--destructive));
|
||||
--color-tertiary: hsl(var(--chart-3));
|
||||
--color-tertiary-container: hsl(var(--chart-3));
|
||||
|
||||
--radius-lg: var(--radius);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
|
||||
--animate-accordion-down: accordion-down 0.2s ease-out;
|
||||
--animate-accordion-up: accordion-up 0.2s ease-out;
|
||||
|
||||
@keyframes accordion-down {
|
||||
from { height: 0; }
|
||||
to { height: var(--radix-accordion-content-height); }
|
||||
}
|
||||
@keyframes accordion-up {
|
||||
from { height: var(--radix-accordion-content-height); }
|
||||
to { height: 0; }
|
||||
}
|
||||
}
|
||||
|
||||
/* Reduced Motion */
|
||||
@layer base {
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
@@ -239,15 +29,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 备课画布:锚点与连线透明度规则 ---- */
|
||||
/* 范围锚定文本背景:默认透明(opacity 0),选中关联节点时显示(opacity 0.3) */
|
||||
/* ---- 备课画布:锚点与连线透明度规则 ---- */
|
||||
/* 范围锚定文本背景:默认透明(opacity 0),选中关联节点时显示(opacity 0.3) */
|
||||
.range-anchor {
|
||||
background-color: var(--node-color, #1976d2);
|
||||
background-color: var(--node-color, hsl(var(--lp-interaction)));
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
border-radius: 2px;
|
||||
transition: opacity var(--duration-normal);
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
padding: 0 1px;
|
||||
padding: 0 var(--space-0_5);
|
||||
}
|
||||
.range-anchor.active {
|
||||
opacity: 0.3;
|
||||
@@ -256,18 +46,18 @@
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* 点锚定占位符:默认半透明(opacity 0.3),选中关联节点时不透明(opacity 1) */
|
||||
/* 点锚定占位符:默认半透明(opacity 0.3),选中关联节点时不透明(opacity 1) */
|
||||
.point-anchor {
|
||||
background-color: var(--node-color, #1976d2);
|
||||
color: #fff;
|
||||
background-color: var(--node-color, hsl(var(--lp-interaction)));
|
||||
color: hsl(var(--background));
|
||||
opacity: 0.3;
|
||||
transition: opacity 0.2s ease;
|
||||
border-radius: 50%;
|
||||
transition: opacity var(--duration-normal);
|
||||
border-radius: var(--radius-full);
|
||||
cursor: pointer;
|
||||
padding: 0 4px;
|
||||
margin: 0 1px;
|
||||
padding: 0 var(--space-1);
|
||||
margin: 0 var(--space-0_5);
|
||||
font-size: 0.85em;
|
||||
font-weight: 600;
|
||||
font-weight: var(--weight-semibold);
|
||||
display: inline-block;
|
||||
min-width: 1.2em;
|
||||
text-align: center;
|
||||
@@ -281,10 +71,10 @@
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* 锚点连线:默认 10% 透明度,选中关联节点时完整显示 */
|
||||
/* 锚点连线:默认 10% 透明度,选中关联节点时完整显示 */
|
||||
.anchor-edge {
|
||||
opacity: 0.1;
|
||||
transition: opacity 0.2s ease;
|
||||
transition: opacity var(--duration-normal);
|
||||
}
|
||||
.anchor-edge.active {
|
||||
opacity: 1;
|
||||
@@ -297,8 +87,8 @@
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* P3-1: 成绩报告卡打印样式(A4 纵向)
|
||||
* 仅在浏览器打印对话框激活时生效,避免影响常规页面渲染。
|
||||
* P3-1: 成绩报告卡打印样式(A4 纵向)
|
||||
* 仅在浏览器打印对话框激活时生效,避免影响常规页面渲染。
|
||||
* ============================================================ */
|
||||
@media print {
|
||||
@page {
|
||||
@@ -326,7 +116,7 @@
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* 表格分页:行不跨页,表整体可分页 */
|
||||
/* 表格分页:行不跨页,表整体可分页 */
|
||||
.report-card-table {
|
||||
page-break-inside: auto;
|
||||
}
|
||||
@@ -343,135 +133,146 @@
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* V4 备课编辑器:纸区工具条 + 锚点 + inline-node + 对话体样式
|
||||
* V4 备课编辑器:纸区工具条 + 锚点 + inline-node + 对话体样式
|
||||
* ============================================================ */
|
||||
|
||||
/* V4 备课编辑器:纸区工具条 */
|
||||
/* V4 备课编辑器:纸区工具条 */
|
||||
.lp-paper-toolbar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
margin: -64px -72px 24px;
|
||||
padding: 10px 72px;
|
||||
background: rgba(255,255,255,0.92);
|
||||
margin: calc(-1 * var(--space-16)) calc(-1 * var(--space-18)) var(--space-6);
|
||||
padding: calc(var(--space-2) + var(--space-0_5)) var(--space-18);
|
||||
background: hsl(var(--background) / 0.92);
|
||||
backdrop-filter: blur(8px);
|
||||
border-bottom: 1px solid var(--border);
|
||||
border-bottom: 1px solid hsl(var(--border));
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
z-index: 5;
|
||||
gap: var(--space-0_5);
|
||||
z-index: var(--z-sticky);
|
||||
}
|
||||
.lp-tb-btn {
|
||||
width: 28px; height: 28px;
|
||||
border: none; background: transparent;
|
||||
border-radius: 4px; cursor: pointer;
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 13px; color: var(--foreground);
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
transition: background .12s;
|
||||
width: var(--space-7);
|
||||
height: var(--space-7);
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-2);
|
||||
color: hsl(var(--foreground));
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background var(--duration-fast);
|
||||
}
|
||||
.lp-tb-btn:hover { background: var(--muted); }
|
||||
.lp-tb-btn.is-active { background: var(--muted); color: var(--foreground); }
|
||||
.lp-tb-btn.b { font-weight: 700; }
|
||||
.lp-tb-btn.i { font-style: italic; font-family: 'Fraunces', serif; }
|
||||
.lp-tb-btn.h1 { font-size: 11px; font-weight: 600; }
|
||||
.lp-tb-btn.h2 { font-size: 10px; font-weight: 600; }
|
||||
.lp-tb-btn.ul { font-size: 14px; }
|
||||
.lp-tb-btn.quote { font-family: 'Fraunces', serif; font-style: italic; font-size: 14px; }
|
||||
.lp-tb-btn:hover { background: hsl(var(--muted)); }
|
||||
.lp-tb-btn.is-active { background: hsl(var(--muted)); color: hsl(var(--foreground)); }
|
||||
.lp-tb-btn.b { font-weight: var(--weight-bold); }
|
||||
.lp-tb-btn.i { font-style: italic; font-family: var(--font-family-serif); }
|
||||
.lp-tb-btn.h1 { font-size: var(--font-size-1); font-weight: var(--weight-semibold); }
|
||||
.lp-tb-btn.h2 { font-size: var(--font-size-1); font-weight: var(--weight-medium); }
|
||||
.lp-tb-btn.ul { font-size: var(--font-size-3); }
|
||||
.lp-tb-btn.quote { font-family: var(--font-family-serif); font-style: italic; font-size: var(--font-size-3); }
|
||||
.lp-tb-divider {
|
||||
width: 1px;
|
||||
background: var(--border);
|
||||
margin: 4px 4px;
|
||||
width: var(--space-0_5);
|
||||
background: hsl(var(--border));
|
||||
margin: var(--space-1) var(--space-1);
|
||||
}
|
||||
|
||||
/* V4 锚点样式 */
|
||||
.lp-anchor-range {
|
||||
background: var(--lp-anchor-range);
|
||||
border-radius: 2px;
|
||||
padding: 1px 2px;
|
||||
margin: 0 -2px;
|
||||
background: hsl(var(--lp-anchor-range));
|
||||
border-radius: var(--radius-sm);
|
||||
padding: var(--space-0_5) var(--space-0_5);
|
||||
margin: 0 calc(-1 * var(--space-0_5));
|
||||
cursor: pointer;
|
||||
border-bottom: 1.5px solid var(--lp-anchor-point);
|
||||
transition: background .15s;
|
||||
border-bottom: 1.5px solid hsl(var(--lp-anchor-point));
|
||||
transition: background var(--duration-fast);
|
||||
}
|
||||
.lp-anchor-range:hover, .lp-anchor-range.is-active {
|
||||
background: var(--lp-anchor-range-active);
|
||||
background: hsl(var(--lp-anchor-range-active));
|
||||
}
|
||||
.lp-anchor-point {
|
||||
display: inline-block;
|
||||
width: 16px; height: 16px;
|
||||
line-height: 16px;
|
||||
width: var(--space-4);
|
||||
height: var(--space-4);
|
||||
line-height: var(--space-4);
|
||||
text-align: center;
|
||||
border-radius: 50%;
|
||||
background: var(--lp-anchor-point);
|
||||
color: #fff;
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 9px;
|
||||
font-weight: 600;
|
||||
margin: 0 2px;
|
||||
border-radius: var(--radius-full);
|
||||
background: hsl(var(--lp-anchor-point));
|
||||
color: hsl(var(--background));
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-1);
|
||||
font-weight: var(--weight-semibold);
|
||||
margin: 0 var(--space-0_5);
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* V4 inline-node 样式 */
|
||||
.lp-inline-node {
|
||||
font-family: 'Inter', sans-serif;
|
||||
margin: 20px 0 20px;
|
||||
padding: 14px 18px 14px 20px;
|
||||
border-left: 2px solid var(--lp-inline-node-border);
|
||||
color: var(--lp-inline-node-text);
|
||||
font-size: 13.5px;
|
||||
line-height: 1.65;
|
||||
transition: border-color .2s;
|
||||
font-family: var(--font-family-sans);
|
||||
margin: var(--space-5) 0 var(--space-5);
|
||||
padding: calc(var(--space-3) + var(--space-0_5)) calc(var(--space-4) + var(--space-2)) calc(var(--space-3) + var(--space-0_5)) var(--space-5);
|
||||
border-left: 2px solid hsl(var(--lp-inline-node-border));
|
||||
color: hsl(var(--lp-inline-node-text));
|
||||
font-size: var(--font-size-3);
|
||||
line-height: var(--leading-relaxed);
|
||||
transition: border-color var(--duration-normal);
|
||||
}
|
||||
.lp-inline-node:hover { border-left-color: var(--foreground); }
|
||||
.lp-inline-node:hover { border-left-color: hsl(var(--foreground)); }
|
||||
.lp-inline-node-head {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
font-size: 10px; font-weight: 600;
|
||||
text-transform: uppercase; letter-spacing: 0.08em;
|
||||
color: var(--lp-inline-node-meta);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
margin-bottom: var(--space-2);
|
||||
font-size: var(--font-size-1);
|
||||
font-weight: var(--weight-semibold);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: hsl(var(--lp-inline-node-meta));
|
||||
}
|
||||
.lp-inline-node-title {
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: #1a1a1a;
|
||||
margin: 0 0 6px;
|
||||
line-height: 1.35;
|
||||
font-family: var(--font-family-sans);
|
||||
font-weight: var(--weight-semibold);
|
||||
font-size: var(--font-size-4);
|
||||
color: hsl(var(--lp-anchor-point));
|
||||
margin: 0 0 var(--space-1_5);
|
||||
line-height: var(--leading-snug);
|
||||
}
|
||||
.lp-inline-node-body {
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: #404040;
|
||||
font-size: var(--font-size-2);
|
||||
line-height: var(--leading-normal);
|
||||
color: hsl(var(--lp-inline-node-text));
|
||||
}
|
||||
|
||||
/* V4 师生交互对话体 */
|
||||
.lp-qa-dialog { margin: 8px 0 4px; }
|
||||
.lp-qa-dialog { margin: var(--space-2) 0 var(--space-1); }
|
||||
.lp-qa-turn {
|
||||
display: grid;
|
||||
grid-template-columns: 28px 1fr;
|
||||
gap: 10px;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px dashed var(--border);
|
||||
font-size: 12.5px;
|
||||
line-height: 1.55;
|
||||
grid-template-columns: calc(var(--space-6) + var(--space-2)) 1fr;
|
||||
gap: calc(var(--space-2) + var(--space-0_5));
|
||||
padding: var(--space-2) 0;
|
||||
border-bottom: 1px dashed hsl(var(--border));
|
||||
font-size: var(--font-size-1);
|
||||
line-height: var(--leading-relaxed);
|
||||
}
|
||||
.lp-qa-turn:last-child { border-bottom: none; }
|
||||
.lp-qa-role {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 9px;
|
||||
font-weight: 600;
|
||||
font-family: var(--font-family-mono);
|
||||
font-size: var(--font-size-0);
|
||||
font-weight: var(--weight-semibold);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
padding-top: 2px;
|
||||
padding-top: var(--space-0_5);
|
||||
text-align: right;
|
||||
}
|
||||
.lp-qa-role.teacher { color: #1c1917; }
|
||||
.lp-qa-role.student { color: #6b7280; }
|
||||
.lp-qa-content { color: #404040; }
|
||||
.lp-qa-role.teacher { color: hsl(var(--lp-anchor-point)); }
|
||||
.lp-qa-role.student { color: hsl(var(--color-zinc-500)); }
|
||||
.lp-qa-content { color: hsl(var(--lp-inline-node-text)); }
|
||||
.lp-qa-prompt {
|
||||
font-style: italic;
|
||||
color: #525252;
|
||||
font-size: 11px;
|
||||
color: hsl(var(--color-zinc-600));
|
||||
font-size: var(--font-size-1);
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
margin-top: var(--space-0_5);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import type { Metadata, Viewport } from "next";
|
||||
import { Inter } from "next/font/google";
|
||||
import { NextIntlClientProvider } from "next-intl";
|
||||
import { getLocale, getMessages } from "next-intl/server";
|
||||
|
||||
import { ThemeProvider } from "@/shared/components/theme-provider";
|
||||
import { Toaster } from "@/shared/components/ui/sonner";
|
||||
import { NuqsAdapter } from "nuqs/adapters/next/app";
|
||||
import { AuthSessionProvider } from "@/shared/components/auth-session-provider";
|
||||
import { Providers } from "./providers";
|
||||
import { auth } from "@/auth";
|
||||
import "./globals.css";
|
||||
|
||||
@@ -33,7 +29,9 @@ export const viewport: Viewport = {
|
||||
maximumScale: 5,
|
||||
userScalable: true,
|
||||
themeColor: [
|
||||
// eslint-disable-next-line no-restricted-syntax -- Next.js metadata requires literal hex
|
||||
{ media: "(prefers-color-scheme: light)", color: "#ffffff" },
|
||||
// eslint-disable-next-line no-restricted-syntax -- Next.js metadata requires literal hex
|
||||
{ media: "(prefers-color-scheme: dark)", color: "#0a0a0a" },
|
||||
],
|
||||
};
|
||||
@@ -57,19 +55,9 @@ export default async function RootLayout({
|
||||
className={`${inter.variable} antialiased font-sans`}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
<NextIntlClientProvider locale={locale} messages={messages}>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<AuthSessionProvider session={session}>
|
||||
<NuqsAdapter>{children}</NuqsAdapter>
|
||||
</AuthSessionProvider>
|
||||
<Toaster />
|
||||
</ThemeProvider>
|
||||
</NextIntlClientProvider>
|
||||
<Providers locale={locale} messages={messages} session={session}>
|
||||
{children}
|
||||
</Providers>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -7,7 +7,9 @@ export default function manifest(): MetadataRoute.Manifest {
|
||||
description: "Enterprise Grade K12 Education Management System",
|
||||
start_url: "/",
|
||||
display: "standalone",
|
||||
// eslint-disable-next-line no-restricted-syntax -- PWA manifest requires literal hex
|
||||
background_color: "#ffffff",
|
||||
// eslint-disable-next-line no-restricted-syntax -- PWA manifest requires literal hex
|
||||
theme_color: "#1976d2",
|
||||
orientation: "any",
|
||||
icons: [
|
||||
|
||||
9
src/app/styles/tokens/index.css
Normal file
9
src/app/styles/tokens/index.css
Normal file
@@ -0,0 +1,9 @@
|
||||
/* src/app/styles/tokens/index.css */
|
||||
/* 设计令牌汇总入口
|
||||
* 加载顺序:Primitive → Semantic(明/暗) → 模块命名空间 → Tailwind 暴露
|
||||
*/
|
||||
@import "./primitive.css";
|
||||
@import "./semantic-light.css";
|
||||
@import "./semantic-dark.css";
|
||||
@import "./lesson-preparation.css";
|
||||
@import "./tailwind-theme.css";
|
||||
81
src/app/styles/tokens/lesson-preparation.css
Normal file
81
src/app/styles/tokens/lesson-preparation.css
Normal file
@@ -0,0 +1,81 @@
|
||||
/* src/app/styles/tokens/lesson-preparation.css */
|
||||
/* --lp-* 令牌(lesson-preparation 模块命名空间)
|
||||
* HEX 已转换为 HSL,与 shadcn 体系统一。
|
||||
* 暗色已补齐。
|
||||
*/
|
||||
|
||||
:root {
|
||||
/* 纸感 */
|
||||
--lp-paper: 0 0% 99.6%;
|
||||
--lp-paper-edge: 60 9% 97%;
|
||||
--lp-paper-shadow: var(--shadow-4);
|
||||
--lp-paper-shadow-active: var(--shadow-5);
|
||||
|
||||
/* 锚点 */
|
||||
--lp-anchor-range: 20 14.3% 4.1% / 0.08;
|
||||
--lp-anchor-range-active: 20 14.3% 4.1% / 0.16;
|
||||
--lp-anchor-point: 20 14.3% 4.1%; /* = stone-950 */
|
||||
|
||||
/* inline-node */
|
||||
--lp-inline-node-border: 30 5.7% 82.9%; /* = stone-300 */
|
||||
--lp-inline-node-text: 24 5.4% 26.9%; /* ≈ stone-700 */
|
||||
--lp-inline-node-meta: 30 5% 64.9%; /* = stone-400 */
|
||||
|
||||
/* 交互强调 */
|
||||
--lp-interaction: 238.6 84.5% 59.8%; /* = indigo-600 */
|
||||
|
||||
/* 节点类型色点(极克制,全部映射到 zinc/stone) */
|
||||
--lp-dot-objective: 240 3.8% 46.1%; /* = zinc-500 */
|
||||
--lp-dot-key-point: 0 0% 30%; /* ≈ zinc-700 灰 */
|
||||
--lp-dot-import: 240 4% 50%; /* = zinc-500 变体 */
|
||||
--lp-dot-new-teaching: 240 10% 3.9%; /* = zinc-950 */
|
||||
--lp-dot-consolidation: 240 4% 50%;
|
||||
--lp-dot-summary: 0 0% 30%;
|
||||
--lp-dot-homework: 0 0% 30%;
|
||||
--lp-dot-blackboard: 24 5.4% 26.9%; /* = stone-700 */
|
||||
--lp-dot-text-study: 240 3.8% 46.1%;
|
||||
--lp-dot-exercise: 240 4% 50%;
|
||||
--lp-dot-rich-text: 30 5% 64.9%; /* = stone-400 */
|
||||
--lp-dot-reflection: 240 4% 50%;
|
||||
--lp-dot-interaction: 238.6 84.5% 59.8%;/* = indigo-600 */
|
||||
--lp-dot-textbook: 24 5.4% 26.9%;
|
||||
--lp-dot-default: 30 5% 64.9%;
|
||||
}
|
||||
|
||||
.dark {
|
||||
/* 纸感(暗背景) */
|
||||
--lp-paper: 240 6% 10%;
|
||||
--lp-paper-edge: 240 6% 8%;
|
||||
--lp-paper-shadow: var(--shadow-4);
|
||||
--lp-paper-shadow-active: var(--shadow-5);
|
||||
|
||||
/* 锚点(反相) */
|
||||
--lp-anchor-range: 0 0% 98% / 0.08;
|
||||
--lp-anchor-range-active: 0 0% 98% / 0.16;
|
||||
--lp-anchor-point: 0 0% 98%;
|
||||
|
||||
/* inline-node */
|
||||
--lp-inline-node-border: 240 4% 30%;
|
||||
--lp-inline-node-text: 240 5% 80%;
|
||||
--lp-inline-node-meta: 240 5% 55%;
|
||||
|
||||
/* 交互强调(暗色微亮) */
|
||||
--lp-interaction: 238.7 83.5% 66.7%; /* = indigo-500 */
|
||||
|
||||
/* 节点类型色点(暗色微亮) */
|
||||
--lp-dot-objective: 240 5% 64.9%;
|
||||
--lp-dot-key-point: 0 0% 70%;
|
||||
--lp-dot-import: 240 4% 65%;
|
||||
--lp-dot-new-teaching: 0 0% 98%;
|
||||
--lp-dot-consolidation: 240 4% 65%;
|
||||
--lp-dot-summary: 0 0% 70%;
|
||||
--lp-dot-homework: 0 0% 70%;
|
||||
--lp-dot-blackboard: 30 5% 70%;
|
||||
--lp-dot-text-study: 240 5% 64.9%;
|
||||
--lp-dot-exercise: 240 4% 65%;
|
||||
--lp-dot-rich-text: 30 5% 55%;
|
||||
--lp-dot-reflection: 240 4% 65%;
|
||||
--lp-dot-interaction: 238.7 83.5% 66.7%;
|
||||
--lp-dot-textbook: 30 5% 70%;
|
||||
--lp-dot-default: 30 5% 55%;
|
||||
}
|
||||
110
src/app/styles/tokens/primitive.css
Normal file
110
src/app/styles/tokens/primitive.css
Normal file
@@ -0,0 +1,110 @@
|
||||
/* src/app/styles/tokens/primitive.css */
|
||||
/* Layer 1: Primitive (Raw Tokens) — 原始色板/字号/间距/阴影/字体家族
|
||||
* 仅被 Semantic 层引用,业务代码不直接使用。
|
||||
* 色板层不区分明暗,主题差异在 Semantic 层体现。
|
||||
*/
|
||||
:root {
|
||||
/* ============ 色板 ============ */
|
||||
|
||||
/* Zinc 中性色板(shadcn 默认) */
|
||||
--color-zinc-50: 0 0% 99%;
|
||||
--color-zinc-100: 240 4.8% 95.9%;
|
||||
--color-zinc-200: 240 5.9% 90%;
|
||||
--color-zinc-300: 240 4.8% 83.9%;
|
||||
--color-zinc-400: 240 5% 64.9%;
|
||||
--color-zinc-500: 240 3.8% 46.1%;
|
||||
--color-zinc-600: 240 5.2% 33.9%;
|
||||
--color-zinc-700: 240 5.3% 26.1%;
|
||||
--color-zinc-800: 240 5.9% 10%;
|
||||
--color-zinc-900: 240 5.9% 3.9%;
|
||||
--color-zinc-950: 240 10% 3.9%;
|
||||
|
||||
/* Stone 暖灰(用于 lp-* 纸感) */
|
||||
--color-stone-50: 60 4.8% 95.9%;
|
||||
--color-stone-100: 60 5.1% 90%;
|
||||
--color-stone-200: 20 5.9% 90%;
|
||||
--color-stone-300: 24 5.7% 82.9%;
|
||||
--color-stone-400: 24 5.4% 63.9%;
|
||||
--color-stone-500: 25 5.1% 44.7%;
|
||||
--color-stone-600: 33 5% 39.8%;
|
||||
--color-stone-700: 30 5.2% 32.7%;
|
||||
--color-stone-800: 12 6.5% 31.4%;
|
||||
--color-stone-900: 24 10% 10%;
|
||||
--color-stone-950: 20 14.3% 4.1%;
|
||||
|
||||
/* Indigo 强调色(lp-interaction 用) */
|
||||
--color-indigo-500: 238.7 83.5% 66.7%;
|
||||
--color-indigo-600: 238.6 84.5% 59.8%;
|
||||
|
||||
/* ============ 字号阶梯 ============ */
|
||||
--font-size-0: 9px; /* 角色标签微字号(lp-qa-role) */
|
||||
--font-size-1: 12px; /* 元信息/角色标签 */
|
||||
--font-size-2: 13px; /* inline-node body */
|
||||
--font-size-3: 13.5px; /* inline-node 主文(备课纸感) */
|
||||
--font-size-4: 14px; /* 标题/按钮 */
|
||||
--font-size-5: 16px; /* 正文(Fraunces 16px) */
|
||||
--font-size-6: 18px; /* H2 */
|
||||
--font-size-7: 20px; /* H1 */
|
||||
--font-size-8: 24px; /* 区块标题 */
|
||||
--font-size-9: 32px; /* 页面标题 */
|
||||
|
||||
/* ============ 间距阶梯 ============ */
|
||||
--space-0: 0;
|
||||
--space-0_5: 0.125rem; /* 2px */
|
||||
--space-1: 0.25rem; /* 4px */
|
||||
--space-1_5: 0.375rem; /* 6px */
|
||||
--space-2: 0.5rem; /* 8px */
|
||||
--space-2_5: 0.625rem; /* 10px */
|
||||
--space-3: 0.75rem; /* 12px */
|
||||
--space-3_5: 0.875rem; /* 14px */
|
||||
--space-4: 1rem; /* 16px */
|
||||
--space-5: 1.25rem; /* 20px */
|
||||
--space-6: 1.5rem; /* 24px */
|
||||
--space-7: 1.75rem; /* 28px */
|
||||
--space-8: 2rem; /* 32px */
|
||||
--space-10: 2.5rem; /* 40px */
|
||||
--space-12: 3rem; /* 48px */
|
||||
--space-16: 4rem; /* 64px */
|
||||
--space-18: 4.5rem; /* 72px */
|
||||
|
||||
/* ============ 阴影阶梯 ============ */
|
||||
--shadow-1: 0 1px 2px rgba(15, 15, 15, 0.04);
|
||||
--shadow-2: 0 1px 3px rgba(15, 15, 15, 0.06), 0 1px 2px rgba(15, 15, 15, 0.04);
|
||||
--shadow-3: 0 4px 6px rgba(15, 15, 15, 0.05), 0 2px 4px rgba(15, 15, 15, 0.04);
|
||||
--shadow-4: 0 1px 2px rgba(15, 15, 15, 0.04), 0 8px 24px rgba(15, 15, 15, 0.04);
|
||||
--shadow-5: 0 1px 2px rgba(15, 15, 15, 0.06), 0 12px 36px rgba(15, 15, 15, 0.08);
|
||||
--shadow-6: 0 10px 15px rgba(15, 15, 15, 0.1), 0 4px 6px rgba(15, 15, 15, 0.05);
|
||||
|
||||
/* ============ 字体家族 ============ */
|
||||
--font-family-sans: 'Inter', system-ui, sans-serif;
|
||||
--font-family-serif: 'Fraunces', Georgia, serif;
|
||||
--font-family-mono: 'JetBrains Mono', monospace;
|
||||
|
||||
/* ============ 行高阶梯 ============ */
|
||||
--leading-tight: 1.2;
|
||||
--leading-snug: 1.35;
|
||||
--leading-normal: 1.5;
|
||||
--leading-relaxed: 1.65;
|
||||
--leading-loose: 1.8;
|
||||
|
||||
/* ============ 字重阶梯 ============ */
|
||||
--weight-regular: 400;
|
||||
--weight-medium: 500;
|
||||
--weight-semibold: 600;
|
||||
--weight-bold: 700;
|
||||
|
||||
/* ============ 动效 ============ */
|
||||
--duration-fast: 150ms;
|
||||
--duration-normal: 200ms;
|
||||
--duration-slow: 300ms;
|
||||
--ease-in: cubic-bezier(0.4, 0, 1, 1);
|
||||
--ease-out: cubic-bezier(0, 0, 0.2, 1);
|
||||
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
/* ============ z-index ============ */
|
||||
--z-dropdown: 1000;
|
||||
--z-sticky: 1100;
|
||||
--z-modal: 1300;
|
||||
--z-popover: 1400;
|
||||
--z-toast: 1500;
|
||||
}
|
||||
65
src/app/styles/tokens/semantic-dark.css
Normal file
65
src/app/styles/tokens/semantic-dark.css
Normal file
@@ -0,0 +1,65 @@
|
||||
/* src/app/styles/tokens/semantic-dark.css */
|
||||
/* Layer 2: Semantic Tokens (Dark / .dark)
|
||||
* 所有令牌都有 :root 对应定义。
|
||||
*/
|
||||
.dark {
|
||||
/* ============ shadcn 标准令牌(原 globals.css 迁移,值不变) ============ */
|
||||
--background: 240 10% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
--card: 240 10% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--popover: 240 10% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 240 5.9% 10%;
|
||||
--secondary: 240 3.7% 15.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted: 240 3.7% 15.9%;
|
||||
--muted-foreground: 240 5% 64.9%;
|
||||
--accent: 240 3.7% 15.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 240 3.7% 15.9%;
|
||||
--input: 240 3.7% 15.9%;
|
||||
--ring: 240 4.9% 83.9%;
|
||||
|
||||
/* ============ chart 令牌(原 globals.css 迁移,值不变) ============ */
|
||||
--chart-1: 220 70% 50%;
|
||||
--chart-2: 160 60% 45%;
|
||||
--chart-3: 30 80% 55%;
|
||||
--chart-4: 280 65% 60%;
|
||||
--chart-5: 340 75% 55%;
|
||||
|
||||
/* ============ sidebar 令牌(原 globals.css 迁移,值不变) ============ */
|
||||
--sidebar-background: 240 5.9% 10%;
|
||||
--sidebar-foreground: 240 4.8% 95.9%;
|
||||
--sidebar-primary: 224.3 76.3% 48%;
|
||||
--sidebar-primary-foreground: 0 0% 100%;
|
||||
--sidebar-accent: 240 3.7% 15.9%;
|
||||
--sidebar-accent-foreground: 240 4.8% 95.9%;
|
||||
--sidebar-border: 240 3.7% 15.9%;
|
||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||
|
||||
/* ============ 语义层扩展(新增层级) ============ */
|
||||
--background-elevated: 240 6% 10%;
|
||||
--background-sunken: 240 6% 8%;
|
||||
--text-primary: 0 0% 98%; /* = --foreground */
|
||||
--text-secondary: 240 5% 64.9%; /* = --muted-foreground */
|
||||
--text-tertiary: 240 5% 50%;
|
||||
--border-strong: 240 5% 40%;
|
||||
--border-subtle: 240 5% 18%;
|
||||
|
||||
/* ============ 新增业务语义令牌 ============ */
|
||||
--diff-add: 142 71% 55%;
|
||||
--diff-add-bg: 142 71% 55% / 0.15;
|
||||
--diff-remove: 0 84% 70%;
|
||||
--diff-remove-bg: 0 84% 70% / 0.15;
|
||||
|
||||
--graph-node-1: 220 70% 50%;
|
||||
--graph-node-2: 160 60% 45%;
|
||||
--graph-node-3: 30 80% 55%;
|
||||
--graph-node-4: 280 65% 60%;
|
||||
--graph-node-5: 340 75% 55%;
|
||||
--graph-node-6: 200 80% 60%;
|
||||
}
|
||||
68
src/app/styles/tokens/semantic-light.css
Normal file
68
src/app/styles/tokens/semantic-light.css
Normal file
@@ -0,0 +1,68 @@
|
||||
/* src/app/styles/tokens/semantic-light.css */
|
||||
/* Layer 2: Semantic Tokens (Light / :root)
|
||||
* 业务代码唯一引用入口。所有令牌都有 .dark 对应定义。
|
||||
*/
|
||||
:root {
|
||||
/* ============ shadcn 标准令牌(原 globals.css 迁移,值不变) ============ */
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 240 10% 3.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 240 10% 3.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 240 10% 3.9%;
|
||||
--primary: 240 5.9% 10%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 240 4.8% 95.9%;
|
||||
--secondary-foreground: 240 5.9% 10%;
|
||||
--muted: 240 4.8% 95.9%;
|
||||
--muted-foreground: 240 3.8% 46.1%;
|
||||
--accent: 240 4.8% 95.9%;
|
||||
--accent-foreground: 240 5.9% 10%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 240 5.9% 90%;
|
||||
--input: 240 5.9% 90%;
|
||||
--ring: 240 5.9% 10%;
|
||||
--radius: 0.5rem;
|
||||
|
||||
/* ============ chart 令牌(原 globals.css 迁移,值不变) ============ */
|
||||
--chart-1: 12 76% 61%;
|
||||
--chart-2: 173 58% 39%;
|
||||
--chart-3: 197 37% 24%;
|
||||
--chart-4: 43 74% 66%;
|
||||
--chart-5: 27 87% 67%;
|
||||
|
||||
/* ============ sidebar 令牌(原 globals.css 迁移,值不变) ============ */
|
||||
--sidebar-background: 0 0% 98%;
|
||||
--sidebar-foreground: 240 5.3% 26.1%;
|
||||
--sidebar-primary: 240 5.9% 10%;
|
||||
--sidebar-primary-foreground: 0 0% 98%;
|
||||
--sidebar-accent: 240 4.8% 95.9%;
|
||||
--sidebar-accent-foreground: 240 5.9% 10%;
|
||||
--sidebar-border: 220 13% 91%;
|
||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||
|
||||
/* ============ 语义层扩展(新增层级) ============ */
|
||||
--background-elevated: 0 0% 100%;
|
||||
--background-sunken: 240 4.8% 95.9%;
|
||||
--text-primary: 240 10% 3.9%; /* = --foreground */
|
||||
--text-secondary: 240 3.8% 46.1%; /* = --muted-foreground */
|
||||
--text-tertiary: 240 4% 65%;
|
||||
--border-strong: 240 5.9% 70%;
|
||||
--border-subtle: 240 5.9% 95%;
|
||||
|
||||
/* ============ 新增业务语义令牌 ============ */
|
||||
/* 版本对比 */
|
||||
--diff-add: 142 71% 45%;
|
||||
--diff-add-bg: 142 71% 45% / 0.1;
|
||||
--diff-remove: 0 84% 60%;
|
||||
--diff-remove-bg: 0 84% 60% / 0.1;
|
||||
|
||||
/* 知识图谱节点色 */
|
||||
--graph-node-1: 12 76% 61%; /* = --chart-1 */
|
||||
--graph-node-2: 173 58% 39%; /* = --chart-2 */
|
||||
--graph-node-3: 197 37% 24%; /* = --chart-3 */
|
||||
--graph-node-4: 43 74% 66%; /* = --chart-4 */
|
||||
--graph-node-5: 27 87% 67%; /* = --chart-5 */
|
||||
--graph-node-6: 280 65% 60%;
|
||||
}
|
||||
148
src/app/styles/tokens/tailwind-theme.css
Normal file
148
src/app/styles/tokens/tailwind-theme.css
Normal file
@@ -0,0 +1,148 @@
|
||||
/* src/app/styles/tokens/tailwind-theme.css */
|
||||
/* @theme inline: 将所有 Semantic 令牌暴露为 Tailwind 类
|
||||
* 业务代码使用 bg-background / text-foreground / bg-lp-paper / font-sans 等
|
||||
*/
|
||||
|
||||
@theme inline {
|
||||
/* ============ shadcn 标准颜色 ============ */
|
||||
--color-background: hsl(var(--background));
|
||||
--color-foreground: hsl(var(--foreground));
|
||||
--color-card: hsl(var(--card));
|
||||
--color-card-foreground: hsl(var(--card-foreground));
|
||||
--color-popover: hsl(var(--popover));
|
||||
--color-popover-foreground: hsl(var(--popover-foreground));
|
||||
--color-primary: hsl(var(--primary));
|
||||
--color-primary-foreground: hsl(var(--primary-foreground));
|
||||
--color-secondary: hsl(var(--secondary));
|
||||
--color-secondary-foreground: hsl(var(--secondary-foreground));
|
||||
--color-muted: hsl(var(--muted));
|
||||
--color-muted-foreground: hsl(var(--muted-foreground));
|
||||
--color-accent: hsl(var(--accent));
|
||||
--color-accent-foreground: hsl(var(--accent-foreground));
|
||||
--color-destructive: hsl(var(--destructive));
|
||||
--color-destructive-foreground: hsl(var(--destructive-foreground));
|
||||
--color-border: hsl(var(--border));
|
||||
--color-input: hsl(var(--input));
|
||||
--color-ring: hsl(var(--ring));
|
||||
|
||||
/* ============ chart 颜色 ============ */
|
||||
--color-chart-1: hsl(var(--chart-1));
|
||||
--color-chart-2: hsl(var(--chart-2));
|
||||
--color-chart-3: hsl(var(--chart-3));
|
||||
--color-chart-4: hsl(var(--chart-4));
|
||||
--color-chart-5: hsl(var(--chart-5));
|
||||
|
||||
/* ============ sidebar 颜色 ============ */
|
||||
--color-sidebar: hsl(var(--sidebar-background));
|
||||
--color-sidebar-foreground: hsl(var(--sidebar-foreground));
|
||||
--color-sidebar-primary: hsl(var(--sidebar-primary));
|
||||
--color-sidebar-primary-foreground: hsl(var(--sidebar-primary-foreground));
|
||||
--color-sidebar-accent: hsl(var(--sidebar-accent));
|
||||
--color-sidebar-accent-foreground: hsl(var(--sidebar-accent-foreground));
|
||||
--color-sidebar-border: hsl(var(--sidebar-border));
|
||||
--color-sidebar-ring: hsl(var(--sidebar-ring));
|
||||
|
||||
/* ============ 扩展语义颜色 ============ */
|
||||
--color-background-elevated: hsl(var(--background-elevated));
|
||||
--color-background-sunken: hsl(var(--background-sunken));
|
||||
--color-text-primary: hsl(var(--text-primary));
|
||||
--color-text-secondary: hsl(var(--text-secondary));
|
||||
--color-text-tertiary: hsl(var(--text-tertiary));
|
||||
--color-border-strong: hsl(var(--border-strong));
|
||||
--color-border-subtle: hsl(var(--border-subtle));
|
||||
--color-diff-add: hsl(var(--diff-add));
|
||||
--color-diff-add-bg: hsl(var(--diff-add-bg));
|
||||
--color-diff-remove: hsl(var(--diff-remove));
|
||||
--color-diff-remove-bg: hsl(var(--diff-remove-bg));
|
||||
--color-graph-node-1: hsl(var(--graph-node-1));
|
||||
--color-graph-node-2: hsl(var(--graph-node-2));
|
||||
--color-graph-node-3: hsl(var(--graph-node-3));
|
||||
--color-graph-node-4: hsl(var(--graph-node-4));
|
||||
--color-graph-node-5: hsl(var(--graph-node-5));
|
||||
--color-graph-node-6: hsl(var(--graph-node-6));
|
||||
|
||||
/* ============ lp-* 颜色(lesson-preparation) ============ */
|
||||
--color-lp-paper: hsl(var(--lp-paper));
|
||||
--color-lp-paper-edge: hsl(var(--lp-paper-edge));
|
||||
--color-lp-anchor-point: hsl(var(--lp-anchor-point));
|
||||
--color-lp-inline-node-border: hsl(var(--lp-inline-node-border));
|
||||
--color-lp-inline-node-text: hsl(var(--lp-inline-node-text));
|
||||
--color-lp-inline-node-meta: hsl(var(--lp-inline-node-meta));
|
||||
--color-lp-interaction: hsl(var(--lp-interaction));
|
||||
--color-lp-dot-objective: hsl(var(--lp-dot-objective));
|
||||
--color-lp-dot-key-point: hsl(var(--lp-dot-key-point));
|
||||
--color-lp-dot-import: hsl(var(--lp-dot-import));
|
||||
--color-lp-dot-new-teaching: hsl(var(--lp-dot-new-teaching));
|
||||
--color-lp-dot-consolidation: hsl(var(--lp-dot-consolidation));
|
||||
--color-lp-dot-summary: hsl(var(--lp-dot-summary));
|
||||
--color-lp-dot-homework: hsl(var(--lp-dot-homework));
|
||||
--color-lp-dot-blackboard: hsl(var(--lp-dot-blackboard));
|
||||
--color-lp-dot-text-study: hsl(var(--lp-dot-text-study));
|
||||
--color-lp-dot-exercise: hsl(var(--lp-dot-exercise));
|
||||
--color-lp-dot-rich-text: hsl(var(--lp-dot-rich-text));
|
||||
--color-lp-dot-reflection: hsl(var(--lp-dot-reflection));
|
||||
--color-lp-dot-interaction: hsl(var(--lp-dot-interaction));
|
||||
--color-lp-dot-textbook: hsl(var(--lp-dot-textbook));
|
||||
--color-lp-dot-default: hsl(var(--lp-dot-default));
|
||||
|
||||
/* ============ 字体家族 ============ */
|
||||
--font-sans: var(--font-family-sans);
|
||||
--font-serif: var(--font-family-serif);
|
||||
--font-mono: var(--font-family-mono);
|
||||
|
||||
/* ============ 字号阶梯 ============ */
|
||||
--text-size-0: var(--font-size-0);
|
||||
--text-size-1: var(--font-size-1);
|
||||
--text-size-2: var(--font-size-2);
|
||||
--text-size-3: var(--font-size-3);
|
||||
--text-size-4: var(--font-size-4);
|
||||
--text-size-5: var(--font-size-5);
|
||||
--text-size-6: var(--font-size-6);
|
||||
--text-size-7: var(--font-size-7);
|
||||
--text-size-8: var(--font-size-8);
|
||||
--text-size-9: var(--font-size-9);
|
||||
|
||||
/* ============ 圆角阶梯 ============ */
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--radius-2xl: calc(var(--radius) + 8px);
|
||||
--radius-full: 9999px;
|
||||
|
||||
/* ============ 阴影阶梯 ============ */
|
||||
--shadow-1: var(--shadow-1);
|
||||
--shadow-2: var(--shadow-2);
|
||||
--shadow-3: var(--shadow-3);
|
||||
--shadow-4: var(--shadow-4);
|
||||
--shadow-5: var(--shadow-5);
|
||||
--shadow-6: var(--shadow-6);
|
||||
|
||||
/* ============ 动效 ============ */
|
||||
--duration-fast: var(--duration-fast);
|
||||
--duration-normal: var(--duration-normal);
|
||||
--duration-slow: var(--duration-slow);
|
||||
--ease-in: var(--ease-in);
|
||||
--ease-out: var(--ease-out);
|
||||
--ease-in-out: var(--ease-in-out);
|
||||
|
||||
/* ============ z-index ============ */
|
||||
--z-dropdown: var(--z-dropdown);
|
||||
--z-sticky: var(--z-sticky);
|
||||
--z-modal: var(--z-modal);
|
||||
--z-popover: var(--z-popover);
|
||||
--z-toast: var(--z-toast);
|
||||
|
||||
/* ============ 动画(原 globals.css 迁移) ============ */
|
||||
--animate-accordion-down: accordion-down 0.2s ease-out;
|
||||
--animate-accordion-up: accordion-up 0.2s ease-out;
|
||||
|
||||
@keyframes accordion-down {
|
||||
from { height: 0; }
|
||||
to { height: var(--radix-accordion-content-height); }
|
||||
}
|
||||
@keyframes accordion-up {
|
||||
from { height: var(--radix-accordion-content-height); }
|
||||
to { height: 0; }
|
||||
}
|
||||
}
|
||||
@@ -245,6 +245,7 @@ function renderLineChart(spec: AiChartSpec, series: AiChartSeries[]): React.Reac
|
||||
axisLine={false}
|
||||
width={36}
|
||||
/>
|
||||
{/* arbitrary-value: chart canvas fixed size */}
|
||||
<ChartTooltip
|
||||
cursor={{
|
||||
stroke: "hsl(var(--muted-foreground))",
|
||||
|
||||
@@ -39,9 +39,9 @@ import type {
|
||||
|
||||
/** 风险等级对应的颜色(散点图着色,与 attendance-sheet 调色板对齐)。 */
|
||||
const RISK_COLORS: Record<AttendanceGradeRiskLevel, string> = {
|
||||
high: "#ef4444", // red-500
|
||||
medium: "#f59e0b", // amber-500
|
||||
low: "#10b981", // emerald-500
|
||||
high: "hsl(var(--chart-1))", // red-500
|
||||
medium: "hsl(var(--chart-4))", // amber-500
|
||||
low: "hsl(var(--chart-2))", // emerald-500
|
||||
}
|
||||
|
||||
/** 风险等级对应的 Badge variant。 */
|
||||
@@ -175,6 +175,7 @@ export function AttendanceGradeCorrelationCard({
|
||||
<h4 className="mb-3 text-sm font-medium">
|
||||
{t("correlation.scatterTitle")}
|
||||
</h4>
|
||||
{/* arbitrary-value: chart canvas fixed size */}
|
||||
<ChartContainer
|
||||
config={chartConfig}
|
||||
className="h-[320px] w-full"
|
||||
|
||||
@@ -94,6 +94,7 @@ export function AttendanceTrendChart({
|
||||
className="border-none shadow-none"
|
||||
/>
|
||||
) : (
|
||||
// arbitrary-value: chart canvas fixed size
|
||||
<TrendLineChart
|
||||
data={chartData}
|
||||
series={series}
|
||||
|
||||
@@ -117,6 +117,7 @@ export function ClassComparisonCard({
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{/* arbitrary-value: chart canvas fixed size */}
|
||||
<SimpleBarChart
|
||||
data={chartData}
|
||||
bars={bars}
|
||||
|
||||
@@ -313,6 +313,7 @@ export function ClassTrendsWidget({ assignments, compact, className }: ClassTren
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{chartData.length > 0 ? (
|
||||
// arbitrary-value: chart canvas fixed size
|
||||
<ChartContainer config={chartConfig} className="h-[250px] w-full">
|
||||
{chartTab === "submission" ? (
|
||||
<LineChart accessibilityLayer data={chartData} margin={{ top: 20, right: 20, bottom: 0, left: 0 }}>
|
||||
@@ -385,6 +386,7 @@ export function ClassTrendsWidget({ assignments, compact, className }: ClassTren
|
||||
)}
|
||||
</ChartContainer>
|
||||
) : (
|
||||
// arbitrary-value: chart canvas fixed size
|
||||
<div className="flex h-[250px] items-center justify-center text-sm text-muted-foreground">
|
||||
{t("detail.trends.noDataForSubject")}
|
||||
</div>
|
||||
|
||||
@@ -107,7 +107,7 @@ export function MyClassesGrid({
|
||||
<DialogContent className="sm:max-w-[480px] p-0 overflow-hidden gap-0 border-none shadow-2xl">
|
||||
{/* Header with Pattern */}
|
||||
<div className="relative bg-primary/5 p-6 border-b border-border/50">
|
||||
<div className="absolute inset-0 opacity-[0.03]" style={{ backgroundImage: 'radial-gradient(#000 1px, transparent 1px)', backgroundSize: '12px 12px' }}></div>
|
||||
<div className="absolute inset-0 opacity-[0.03]" style={{ backgroundImage: 'radial-gradient(hsl(var(--color-zinc-950)) 1px, transparent 1px)', backgroundSize: '12px 12px' }}></div>
|
||||
<DialogHeader className="relative z-10">
|
||||
<DialogTitle className="text-xl font-bold flex items-center gap-2">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary text-primary-foreground shadow-sm">
|
||||
@@ -275,7 +275,7 @@ function ClassTicket({
|
||||
<div className="group relative flex w-full overflow-hidden rounded-xl border bg-card shadow-sm transition-all hover:shadow-md">
|
||||
{/* Realistic Paper Texture & Noise */}
|
||||
<div className="absolute inset-0 pointer-events-none opacity-[0.02]" style={{ backgroundImage: 'url("data:image/svg+xml,%3Csvg viewBox=\'0 0 200 200\' xmlns=\'http://www.w3.org/2000/svg\'%3E%3Cfilter id=\'noiseFilter\'%3E%3CfeTurbulence type=\'fractalNoise\' baseFrequency=\'0.65\' numOctaves=\'3\' stitchTiles=\'stitch\'/%3E%3C/filter%3E%3Crect width=\'100%25\' height=\'100%25\' filter=\'url(%23noiseFilter)\'/%3E%3C/svg%3E")' }}></div>
|
||||
<div className="absolute inset-0 pointer-events-none opacity-[0.03]" style={{ backgroundImage: 'radial-gradient(#000 1px, transparent 1px)', backgroundSize: '16px 16px' }}></div>
|
||||
<div className="absolute inset-0 pointer-events-none opacity-[0.03]" style={{ backgroundImage: 'radial-gradient(hsl(var(--color-zinc-950)) 1px, transparent 1px)', backgroundSize: '16px 16px' }}></div>
|
||||
|
||||
{/* Decorative Barcode Strip */}
|
||||
<div className="absolute left-0 top-0 bottom-0 w-1.5 bg-primary/10 flex flex-col justify-between py-2 pointer-events-none">
|
||||
@@ -329,8 +329,8 @@ function ClassTicket({
|
||||
{/* Invitation Code Section */}
|
||||
<div className="mt-6 pt-4 border-t border-dashed border-border relative">
|
||||
{/* Tiny Cut marks */}
|
||||
<div className="absolute -left-5 top-[-1px] w-2 h-[2px] bg-border"></div>
|
||||
<div className="absolute -right-5 top-[-1px] w-2 h-[2px] bg-border"></div>
|
||||
<div className="absolute -left-5 top-[-1px] w-2 h-0.5 bg-border"></div>
|
||||
<div className="absolute -right-5 top-[-1px] w-2 h-0.5 bg-border"></div>
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div className="flex items-center justify-between">
|
||||
|
||||
@@ -143,7 +143,7 @@ export function CoursePlanCalendar({ plan }: CoursePlanCalendarProps): JSX.Eleme
|
||||
aria-label={day.toDateString()}
|
||||
aria-current={isToday ? "date" : undefined}
|
||||
className={cn(
|
||||
"min-h-[80px] rounded-md border p-1 text-left",
|
||||
"min-h-20 rounded-md border p-1 text-left",
|
||||
inMonth ? "bg-card" : "bg-muted/30",
|
||||
isToday && "ring-2 ring-primary",
|
||||
)}
|
||||
|
||||
@@ -147,7 +147,7 @@ export function TemplatePickerDialog({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="h-[320px] rounded-md border">
|
||||
<ScrollArea className="h-80 rounded-md border">
|
||||
{loading ? (
|
||||
<div
|
||||
className="flex h-full items-center justify-center gap-2 p-8 text-sm text-muted-foreground"
|
||||
|
||||
@@ -296,7 +296,7 @@ export function ElectiveCourseForm({
|
||||
id="description"
|
||||
name="description"
|
||||
placeholder={t("form.descriptionPlaceholder")}
|
||||
className="min-h-[80px]"
|
||||
className="min-h-20"
|
||||
defaultValue={course?.description ?? ""}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -73,6 +73,7 @@ export function ChapterWeaknessChart({ data, className }: ChapterWeaknessChartPr
|
||||
<CardTitle className="text-base">{t("chapterChart.title")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* arbitrary-value: chart canvas fixed size */}
|
||||
<ChartContainer
|
||||
config={chartConfig}
|
||||
className="h-[300px] w-full"
|
||||
|
||||
@@ -81,6 +81,7 @@ export function ClassErrorBarChart({ data, className }: ClassErrorBarChartProps)
|
||||
<CardTitle className="text-base">{t("classErrorBar.title")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{/* arbitrary-value: chart canvas fixed size */}
|
||||
<ChartContainer
|
||||
config={chartConfig}
|
||||
className="h-[280px] w-full"
|
||||
|
||||
@@ -245,7 +245,7 @@ export function ErrorBookDetailDialog({
|
||||
value={note}
|
||||
onChange={(e) => setNote(e.target.value)}
|
||||
placeholder={t("detailDialog.notePlaceholder")}
|
||||
className="w-full min-h-[80px] rounded-md border bg-background p-3 text-sm resize-y focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
className="w-full min-h-20 rounded-md border bg-background p-3 text-sm resize-y focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
maxLength={2000}
|
||||
/>
|
||||
<div className="mt-2">
|
||||
|
||||
@@ -102,7 +102,7 @@ export function KnowledgePointWeaknessChart({
|
||||
<ChartTooltip
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
className="w-[240px]"
|
||||
className="w-60"
|
||||
formatter={(payload: unknown) => {
|
||||
if (!isKpChartPayload(payload)) return null
|
||||
return (
|
||||
|
||||
@@ -78,6 +78,7 @@ export function SubjectDistributionChart({
|
||||
<CardTitle className="text-base">{t("subjectDistChart.title")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{/* arbitrary-value: chart canvas fixed size */}
|
||||
<ChartContainer
|
||||
config={chartConfig}
|
||||
className="h-[280px] w-full"
|
||||
|
||||
@@ -111,7 +111,7 @@ export function ExamPreviewDialog({
|
||||
)}
|
||||
>
|
||||
<div className="flex gap-2">
|
||||
<span className="font-semibold text-foreground min-w-[28px]">{questionNumber}.</span>
|
||||
<span className="font-semibold text-foreground min-w-7">{questionNumber}.</span>
|
||||
<div className="flex-1 space-y-2">
|
||||
<div className="text-foreground/90 leading-relaxed whitespace-pre-wrap">
|
||||
{content.text || t("exam.previewDialog.untitledQuestion")}
|
||||
@@ -121,7 +121,7 @@ export function ExamPreviewDialog({
|
||||
<div className="space-y-1.5">
|
||||
{content.options.map((opt) => (
|
||||
<div key={`${node.id}-${opt.id}`} className="text-sm text-foreground/80 flex gap-2">
|
||||
<span className="min-w-[28px]">{opt.id}.</span>
|
||||
<span className="min-w-7">{opt.id}.</span>
|
||||
<span className="whitespace-pre-wrap">{opt.text}</span>
|
||||
</div>
|
||||
))}
|
||||
@@ -131,7 +131,7 @@ export function ExamPreviewDialog({
|
||||
<div className="space-y-1.5 rounded-md bg-muted/40 p-2">
|
||||
{content.subQuestions.map((item, index) => (
|
||||
<div key={`${node.id}-sub-${index}`} className="text-sm text-foreground/80 flex gap-2">
|
||||
<span className="min-w-[28px]">{item.id}.</span>
|
||||
<span className="min-w-7">{item.id}.</span>
|
||||
<span className="whitespace-pre-wrap">{item.text || t("exam.previewDialog.untitledSubQuestion")}</span>
|
||||
{item.score ? <span className="text-xs text-muted-foreground">({item.score}{t("exam.previewDialog.scoreUnit")})</span> : null}
|
||||
</div>
|
||||
|
||||
@@ -328,7 +328,7 @@ export function HomeworkGradingView({
|
||||
placeholder={t("homework.grade.feedbackPlaceholder", { name: studentName })}
|
||||
value={ans.feedback ?? ""}
|
||||
onChange={(e) => handleFeedbackChange(ans.id, e.target.value)}
|
||||
className="min-h-[80px] bg-background"
|
||||
className="min-h-20 bg-background"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -63,6 +63,7 @@ export function AiDifferentiationDialog({ doc, textbookId, onClose }: Props) {
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/30">
|
||||
{/* arbitrary-value: dialog fixed width */}
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
|
||||
@@ -56,6 +56,7 @@ export function AiFeedbackDialog({ doc, onClose }: Props) {
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/30">
|
||||
{/* arbitrary-value: dialog fixed width */}
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
|
||||
@@ -57,14 +57,14 @@ function BannerContent({
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
background: "#fef3c7",
|
||||
borderBottom: "1px solid #fcd34d",
|
||||
background: "hsl(var(--color-stone-200))",
|
||||
borderBottom: "1px solid hsl(var(--color-stone-400))",
|
||||
padding: "8px 20px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 12,
|
||||
fontSize: 12.5,
|
||||
color: "#92400e",
|
||||
color: "hsl(var(--color-stone-700))",
|
||||
}}
|
||||
>
|
||||
<strong style={{ fontWeight: 600 }}>{t("v4.migration.legacyAnchorTitle")}</strong>
|
||||
@@ -74,11 +74,11 @@ function BannerContent({
|
||||
onClick={onDismiss}
|
||||
style={{
|
||||
background: "transparent",
|
||||
border: "1px solid #fcd34d",
|
||||
border: "1px solid hsl(var(--color-stone-400))",
|
||||
borderRadius: 3,
|
||||
padding: "3px 10px",
|
||||
fontSize: 11,
|
||||
color: "#92400e",
|
||||
color: "hsl(var(--color-stone-700))",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -157,6 +157,7 @@ export function AttachmentPicker({
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/30">
|
||||
{/* arbitrary-value: dialog fixed width */}
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
|
||||
@@ -97,6 +97,7 @@ export function BlackboardBlock({ data, textbookId, chapterId, onUpdate }: Props
|
||||
<label className="text-xs font-medium block mb-1">
|
||||
{t("blackboard.contentLabel")}
|
||||
</label>
|
||||
{/* arbitrary-value: textarea min height */}
|
||||
<textarea
|
||||
value={data.content}
|
||||
onChange={(e) => onUpdate({ ...data, content: e.target.value })}
|
||||
@@ -166,6 +167,7 @@ function BlackboardPreview({
|
||||
|
||||
if (layout === "text") {
|
||||
return (
|
||||
// arbitrary-value: content min height
|
||||
<pre
|
||||
className="text-sm font-mono whitespace-pre-wrap border border-outline-variant rounded p-3 bg-surface-container-low min-h-[120px]"
|
||||
>
|
||||
@@ -178,6 +180,7 @@ function BlackboardPreview({
|
||||
const center = parsed[0]?.text ?? "";
|
||||
const branches = parsed.slice(1);
|
||||
return (
|
||||
// arbitrary-value: content min height
|
||||
<div className="border border-outline-variant rounded p-3 bg-surface-container-low min-h-[120px]">
|
||||
{/* 中心节点 */}
|
||||
<div className="flex justify-center mb-3">
|
||||
@@ -208,6 +211,7 @@ function BlackboardPreview({
|
||||
|
||||
// structure:层级树
|
||||
return (
|
||||
// arbitrary-value: content min height
|
||||
<div className="border border-outline-variant rounded p-3 bg-surface-container-low min-h-[120px]">
|
||||
<ul className="space-y-1">
|
||||
{parsed.map((node, idx) => (
|
||||
|
||||
@@ -63,7 +63,7 @@ export function KeyPointBlock({ data, onUpdate }: Props) {
|
||||
<textarea
|
||||
value={item.text}
|
||||
onChange={(e) => updateItem(idx, { text: e.target.value })}
|
||||
className="flex-1 text-sm border border-outline-variant rounded px-2 py-1 resize-y min-h-[40px]"
|
||||
className="flex-1 text-sm border border-outline-variant rounded px-2 py-1 resize-y min-h-10"
|
||||
placeholder={t("keyPoint.textPlaceholder")}
|
||||
/>
|
||||
<Button
|
||||
|
||||
@@ -68,6 +68,7 @@ export function NewTeachingBlock({ data, textbookId, chapterId, onUpdate }: Prop
|
||||
<label className="text-xs block mb-1">
|
||||
{t("newTeaching.outlineLabel")}
|
||||
</label>
|
||||
{/* arbitrary-value: textarea min height */}
|
||||
<textarea
|
||||
value={point.outline}
|
||||
onChange={(e) => updatePoint(idx, { outline: e.target.value })}
|
||||
@@ -82,7 +83,7 @@ export function NewTeachingBlock({ data, textbookId, chapterId, onUpdate }: Prop
|
||||
<textarea
|
||||
value={point.boardNotes}
|
||||
onChange={(e) => updatePoint(idx, { boardNotes: e.target.value })}
|
||||
className="w-full text-sm border border-outline-variant rounded px-2 py-1 resize-y min-h-[40px]"
|
||||
className="w-full text-sm border border-outline-variant rounded px-2 py-1 resize-y min-h-10"
|
||||
placeholder={t("newTeaching.boardNotesPlaceholder")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -66,7 +66,7 @@ export function ObjectiveBlock({ data, onUpdate }: Props) {
|
||||
<textarea
|
||||
value={item.text}
|
||||
onChange={(e) => updateItem(idx, { text: e.target.value })}
|
||||
className="flex-1 text-sm border border-outline-variant rounded px-2 py-1 resize-y min-h-[40px]"
|
||||
className="flex-1 text-sm border border-outline-variant rounded px-2 py-1 resize-y min-h-10"
|
||||
placeholder={t("objective.textPlaceholder")}
|
||||
/>
|
||||
<Button
|
||||
|
||||
@@ -60,6 +60,7 @@ export function ReflectionBlock({ data, onUpdate }: Props) {
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{/* arbitrary-value: textarea min height */}
|
||||
<textarea
|
||||
value={item.text}
|
||||
onChange={(e) => updateItem(idx, { text: e.target.value })}
|
||||
|
||||
@@ -54,6 +54,7 @@ export function RichTextBlock({
|
||||
},
|
||||
editorProps: {
|
||||
attributes: {
|
||||
// arbitrary-value: tiptap editor fixed size
|
||||
class:
|
||||
"prose prose-sm max-w-none focus:outline-none min-h-[60px] px-3 py-2",
|
||||
},
|
||||
|
||||
@@ -69,7 +69,7 @@ export function SummaryBlock({ data, onUpdate }: Props) {
|
||||
<textarea
|
||||
value={data.homeworkPreview}
|
||||
onChange={(e) => onUpdate({ ...data, homeworkPreview: e.target.value })}
|
||||
className="w-full text-sm border border-outline-variant rounded px-2 py-1 resize-y min-h-[40px]"
|
||||
className="w-full text-sm border border-outline-variant rounded px-2 py-1 resize-y min-h-10"
|
||||
placeholder={t("summary.homeworkPreviewPlaceholder")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -307,6 +307,7 @@ function WeekGrid({ days, grouped, today, loading, t }: GridProps): JSX.Element
|
||||
{t(`calendar.weekDays.${WEEK_DAY_KEYS[d.getDay()]}`)}
|
||||
<span className="ml-1">{d.getDate()}</span>
|
||||
</div>
|
||||
{/* arbitrary-value: calendar cell min height */}
|
||||
<div className="flex-1 min-h-[200px] space-y-1 pt-1">
|
||||
{loading ? (
|
||||
<Skeleton className="h-8 w-full" />
|
||||
@@ -347,6 +348,7 @@ function MonthGrid({ days, cursor, grouped, today, loading, t }: GridProps & { c
|
||||
const isCurrentMonth = d.getMonth() === cursor.getMonth();
|
||||
const isToday = isSameDay(d, today);
|
||||
return (
|
||||
// arbitrary-value: calendar cell min height
|
||||
<div
|
||||
key={key}
|
||||
className={cn(
|
||||
|
||||
@@ -36,6 +36,7 @@ export function ConsistencyCheckDialog({ doc, onClose }: Props) {
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/30">
|
||||
{/* arbitrary-value: dialog fixed width */}
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
|
||||
@@ -46,13 +46,14 @@ export function CurriculumMapView({ grades, subjects, heatmap }: Props): JSX.Ele
|
||||
<table className="w-full border-collapse text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
{/* arbitrary-value: table column min width */}
|
||||
<th className="border border-border bg-muted/50 p-2 text-left font-medium sticky left-0 z-10 min-w-[100px]">
|
||||
学科\年级
|
||||
</th>
|
||||
{grades.map((g) => (
|
||||
<th
|
||||
key={g.id}
|
||||
className="border border-border bg-muted/50 p-2 text-center font-medium min-w-[80px]"
|
||||
className="border border-border bg-muted/50 p-2 text-center font-medium min-w-20"
|
||||
>
|
||||
{g.name}
|
||||
</th>
|
||||
@@ -62,6 +63,7 @@ export function CurriculumMapView({ grades, subjects, heatmap }: Props): JSX.Ele
|
||||
<tbody>
|
||||
{subjects.map((s) => (
|
||||
<tr key={s.id}>
|
||||
{/* arbitrary-value: table column min width */}
|
||||
<td className="border border-border p-2 font-medium sticky left-0 z-10 bg-card min-w-[100px]">
|
||||
{s.name}
|
||||
</td>
|
||||
|
||||
@@ -58,7 +58,7 @@ export function DetailHead({ node, isExpanded }: Props) {
|
||||
background: "transparent",
|
||||
border: "none",
|
||||
outline: "none",
|
||||
fontFamily: "Inter, sans-serif",
|
||||
fontFamily: "var(--font-family-sans)",
|
||||
fontWeight: 600,
|
||||
fontSize: 16,
|
||||
color: "var(--foreground)",
|
||||
|
||||
@@ -84,7 +84,7 @@ export function QaEditor({ nodeId, data }: Props) {
|
||||
borderRadius: 5,
|
||||
padding: "8px 10px",
|
||||
background: "var(--background)",
|
||||
fontFamily: "Inter, sans-serif",
|
||||
fontFamily: "var(--font-family-sans)",
|
||||
fontSize: 13,
|
||||
lineHeight: 1.6,
|
||||
color: "var(--foreground)",
|
||||
@@ -181,7 +181,7 @@ function QaTurnEditor({ turn, index, total, onUpdate, onRemove, onMoveUp, onMove
|
||||
onChange={(e) => onUpdate({ role: e.target.value as "teacher" | "student" })}
|
||||
className={isTeacher ? "teacher" : "student"}
|
||||
style={{
|
||||
fontFamily: "JetBrains Mono, monospace",
|
||||
fontFamily: "var(--font-family-mono)",
|
||||
fontSize: 9,
|
||||
fontWeight: 600,
|
||||
padding: "2px 6px",
|
||||
@@ -189,7 +189,7 @@ function QaTurnEditor({ turn, index, total, onUpdate, onRemove, onMoveUp, onMove
|
||||
border: "1px solid var(--border)",
|
||||
background: "var(--background)",
|
||||
cursor: "pointer",
|
||||
color: isTeacher ? "#1c1917" : "#6b7280",
|
||||
color: isTeacher ? "hsl(var(--foreground))" : "hsl(var(--muted-foreground))",
|
||||
}}
|
||||
>
|
||||
<option value="teacher">{t("v4.detail.turnTeacher")}</option>
|
||||
@@ -215,7 +215,7 @@ function QaTurnEditor({ turn, index, total, onUpdate, onRemove, onMoveUp, onMove
|
||||
outline: "none",
|
||||
background: "transparent",
|
||||
resize: "vertical",
|
||||
fontFamily: "Inter, sans-serif",
|
||||
fontFamily: "var(--font-family-sans)",
|
||||
fontSize: 12.5,
|
||||
lineHeight: 1.5,
|
||||
color: "var(--foreground)",
|
||||
@@ -234,7 +234,7 @@ function QaTurnEditor({ turn, index, total, onUpdate, onRemove, onMoveUp, onMove
|
||||
outline: "none",
|
||||
background: "var(--muted)",
|
||||
resize: "vertical",
|
||||
fontFamily: "Inter, sans-serif",
|
||||
fontFamily: "var(--font-family-sans)",
|
||||
fontSize: 11,
|
||||
fontStyle: "italic",
|
||||
color: "var(--muted-foreground)",
|
||||
|
||||
@@ -77,6 +77,7 @@ export function InlineQuestionEditor({ onAdd, onClose, textbookId, chapterId }:
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/30">
|
||||
{/* arbitrary-value: dialog fixed width */}
|
||||
<div
|
||||
className="bg-surface rounded-lg shadow-xl w-[600px] max-h-[80vh] flex flex-col"
|
||||
role="dialog"
|
||||
@@ -118,7 +119,7 @@ export function InlineQuestionEditor({ onAdd, onClose, textbookId, chapterId }:
|
||||
id="inline-question-stem"
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
className="w-full border rounded px-2 py-1 mt-1 min-h-[80px]"
|
||||
className="w-full border rounded px-2 py-1 mt-1 min-h-20"
|
||||
/>
|
||||
</div>
|
||||
{type === "single_choice" && (
|
||||
|
||||
@@ -75,11 +75,14 @@ export function LessonPlanEditor({
|
||||
const versionTimer = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
|
||||
// 初始化:仅在 planId 变化时 hydrate(修复 P1-3)
|
||||
const initKey = planId;
|
||||
// hydrate 是 zustand store action,引用稳定
|
||||
// initialTitle/initialDoc 通过 ref 保存,避免作为 effect 依赖导致重复 hydrate
|
||||
const hydrate = useLessonPlanEditor((s) => s.hydrate);
|
||||
const initialRef = useRef({ initialTitle, initialDoc });
|
||||
useEffect(() => {
|
||||
useLessonPlanEditor.getState().hydrate(planId, initialTitle, initialDoc);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [initKey]);
|
||||
const { initialTitle: t0, initialDoc: d0 } = initialRef.current;
|
||||
hydrate(planId, t0, d0);
|
||||
}, [planId, hydrate]);
|
||||
|
||||
// 自动保存(debounce 3s)- 用 getState() 获取最新值(修复 P1-4)
|
||||
// V3 修复:完全通过 service 调用,不直接 import actions
|
||||
@@ -332,11 +335,13 @@ export function LessonPlanEditor({
|
||||
{textbookTitle && (
|
||||
<div className="flex items-center gap-1 text-xs text-on-surface-variant px-2 py-1 rounded bg-surface-container-high">
|
||||
<Book className="w-3 h-3" />
|
||||
{/* arbitrary-value: layout fixed size */}
|
||||
<span className="max-w-[120px] truncate">{textbookTitle}</span>
|
||||
{chapterTitle && (
|
||||
<>
|
||||
<span className="text-on-surface-variant/50">/</span>
|
||||
<FileText className="w-3 h-3" />
|
||||
{/* arbitrary-value: layout fixed size */}
|
||||
<span className="max-w-[120px] truncate">{chapterTitle}</span>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -124,6 +124,7 @@ function MobileNodeCard({
|
||||
{node.title || node.type}
|
||||
</h4>
|
||||
{diff && (
|
||||
// arbitrary-value: badge font size
|
||||
<span
|
||||
className={cn(
|
||||
"text-[10px] px-1.5 py-0.5 rounded font-medium flex-shrink-0",
|
||||
|
||||
@@ -39,7 +39,7 @@ export function InlineNode({ node }: Props) {
|
||||
}}
|
||||
/>
|
||||
<span>{node.type}</span>
|
||||
<span style={{ marginLeft: "auto", fontFamily: "JetBrains Mono, monospace", fontSize: 9, color: "var(--lp-inline-node-meta)" }}>
|
||||
<span style={{ marginLeft: "auto", fontFamily: "var(--font-family-mono)", fontSize: 9, color: "var(--lp-inline-node-meta)" }}>
|
||||
{node.id.slice(-4)}
|
||||
</span>
|
||||
<button
|
||||
|
||||
@@ -32,7 +32,7 @@ export function InlineQaDialog({ node }: Props) {
|
||||
}}
|
||||
/>
|
||||
<span>{t("v4.interaction.label")}</span>
|
||||
<span style={{ marginLeft: "auto", fontFamily: "JetBrains Mono, monospace", fontSize: 9, color: "var(--lp-inline-node-meta)" }}>
|
||||
<span style={{ marginLeft: "auto", fontFamily: "var(--font-family-mono)", fontSize: 9, color: "var(--lp-inline-node-meta)" }}>
|
||||
{data.turns?.length ?? 0} 轮
|
||||
</span>
|
||||
<button
|
||||
|
||||
@@ -55,7 +55,7 @@ export function PaperContextMenu({ state, onClose, onAiAction }: Props) {
|
||||
};
|
||||
|
||||
const copyNode = async (nodeId: string) => {
|
||||
const newId = duplicateNode(nodeId);
|
||||
const newId = duplicateNode(nodeId, t("v4.contextMenu.copySuffix"));
|
||||
const { toast } = await import("sonner");
|
||||
if (newId) {
|
||||
toast.success(t("v4.contextMenu.copied"));
|
||||
@@ -95,7 +95,7 @@ export function PaperContextMenu({ state, onClose, onAiAction }: Props) {
|
||||
padding: "7px 10px",
|
||||
borderRadius: 4,
|
||||
cursor: "pointer",
|
||||
color: opts?.danger ? "#dc2626" : opts?.ai ? "var(--lp-interaction)" : "var(--foreground)",
|
||||
color: opts?.danger ? "hsl(var(--destructive))" : opts?.ai ? "var(--lp-interaction)" : "var(--foreground)",
|
||||
background: "transparent",
|
||||
border: "none",
|
||||
width: "100%",
|
||||
@@ -106,7 +106,7 @@ export function PaperContextMenu({ state, onClose, onAiAction }: Props) {
|
||||
>
|
||||
<span style={{ flex: 1 }}>{label}</span>
|
||||
{opts?.shortcut && (
|
||||
<span style={{ fontFamily: "JetBrains Mono, monospace", fontSize: 10, color: "var(--muted-foreground)" }}>
|
||||
<span style={{ fontFamily: "var(--font-family-mono)", fontSize: 10, color: "var(--muted-foreground)" }}>
|
||||
{opts.shortcut}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -100,15 +100,15 @@ export function PaperEditor({ readonly }: { readonly?: boolean }) {
|
||||
borderRadius: 2,
|
||||
border: "1px solid var(--lp-paper-edge)",
|
||||
minHeight: 800,
|
||||
fontFamily: "'Fraunces', Georgia, serif",
|
||||
color: "#1a1a1a",
|
||||
fontFamily: "var(--font-family-serif)",
|
||||
color: "hsl(var(--lp-anchor-point))",
|
||||
lineHeight: 1.7,
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: "Inter, sans-serif",
|
||||
fontFamily: "var(--font-family-sans)",
|
||||
fontSize: 11,
|
||||
color: "var(--muted-foreground)",
|
||||
letterSpacing: "0.08em",
|
||||
|
||||
@@ -48,7 +48,7 @@ export function TextbookTiptapEditor({ content, readonly }: Props) {
|
||||
editorProps: {
|
||||
attributes: {
|
||||
class: "lp-textbook-editor prose prose-sm max-w-none focus:outline-none",
|
||||
style: "font-family: 'Fraunces', Georgia, serif; font-size: 16px; line-height: 1.75; color: #1a1a1a;",
|
||||
style: "font-family: var(--font-family-serif); font-size: var(--font-size-5); line-height: 1.75; color: hsl(var(--lp-anchor-point));",
|
||||
},
|
||||
},
|
||||
onUpdate: ({ editor: e }) => {
|
||||
|
||||
@@ -50,8 +50,9 @@ export function PrintView({
|
||||
plan,
|
||||
{ textbookTitle, chapterTitle, teacherName, className },
|
||||
variant,
|
||||
(key, params) => t(key, params),
|
||||
),
|
||||
[plan, textbookTitle, chapterTitle, teacherName, className, variant],
|
||||
[plan, textbookTitle, chapterTitle, teacherName, className, variant, t],
|
||||
);
|
||||
|
||||
function handlePrint() {
|
||||
@@ -60,6 +61,7 @@ export function PrintView({
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 bg-black/30 flex items-center justify-center print:static print:bg-white print:p-0">
|
||||
{/* arbitrary-value: dialog fixed width */}
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
|
||||
@@ -143,6 +143,7 @@ export function PublishHomeworkDialog({
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/30">
|
||||
{/* arbitrary-value: dialog fixed width */}
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
|
||||
@@ -169,6 +169,7 @@ export function QuestionBankPicker({ onPick, onClose, existingIds }: Props) {
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/30">
|
||||
{/* arbitrary-value: dialog fixed width */}
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { X, Calendar, Trash2, Plus } from "lucide-react";
|
||||
import { Button } from "@/shared/components/ui/button";
|
||||
@@ -48,19 +48,7 @@ export function ScheduleDialog({ planId, classes, onClose, onScheduled }: Props)
|
||||
const [durationMin, setDurationMin] = useState(40);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
void loadSchedules();
|
||||
}, [planId]);
|
||||
|
||||
useEffect(() => {
|
||||
function handleEsc(e: KeyboardEvent) {
|
||||
if (e.key === "Escape") onClose();
|
||||
}
|
||||
document.addEventListener("keydown", handleEsc);
|
||||
return () => document.removeEventListener("keydown", handleEsc);
|
||||
}, [onClose]);
|
||||
|
||||
async function loadSchedules() {
|
||||
const loadSchedules = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await getLessonPlanSchedulesAction(planId);
|
||||
@@ -73,7 +61,19 @@ export function ScheduleDialog({ planId, classes, onClose, onScheduled }: Props)
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
}, [planId, t]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadSchedules();
|
||||
}, [loadSchedules]);
|
||||
|
||||
useEffect(() => {
|
||||
function handleEsc(e: KeyboardEvent) {
|
||||
if (e.key === "Escape") onClose();
|
||||
}
|
||||
document.addEventListener("keydown", handleEsc);
|
||||
return () => document.removeEventListener("keydown", handleEsc);
|
||||
}, [onClose]);
|
||||
|
||||
async function handleAdd() {
|
||||
if (!classId) {
|
||||
@@ -127,6 +127,7 @@ export function ScheduleDialog({ planId, classes, onClose, onScheduled }: Props)
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/30">
|
||||
{/* arbitrary-value: dialog fixed width */}
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
|
||||
@@ -65,12 +65,9 @@ export function TemplatePicker() {
|
||||
const [personalTemplates, setPersonalTemplates] = useState<LessonPlanTemplate[]>([]);
|
||||
// V5-9:教材搜索 + 最近使用
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [recentIds, setRecentIds] = useState<string[]>([]);
|
||||
|
||||
// V5-9:客户端挂载后读取最近使用教材
|
||||
useEffect(() => {
|
||||
setRecentIds(readRecentTextbookIds());
|
||||
}, []);
|
||||
// V5-9:使用 lazy initializer 在首次渲染时读取 localStorage,
|
||||
// 避免 effect 内同步 setState 触发级联渲染(react-hooks/set-state-in-effect)
|
||||
const [recentIds, setRecentIds] = useState<string[]>(() => readRecentTextbookIds());
|
||||
|
||||
// V5-9:客户端模糊搜索过滤教材(标题/学科/年级/出版社)
|
||||
const filteredTextbooks = useMemo(() => {
|
||||
@@ -418,6 +415,7 @@ export function TemplatePicker() {
|
||||
>
|
||||
<div className="font-title-md flex items-center gap-2">
|
||||
<span className="truncate">{tpl.name}</span>
|
||||
{/* arbitrary-value: badge font size */}
|
||||
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-secondary text-secondary-foreground flex-shrink-0">
|
||||
{t("template.personalBadge")}
|
||||
</span>
|
||||
|
||||
@@ -78,7 +78,7 @@ function DiffItem({
|
||||
t: ReturnType<typeof useTranslations>;
|
||||
}) {
|
||||
const node = diff.newNode ?? diff.oldNode;
|
||||
const color = node ? getNodeColor(node.type) : "#999";
|
||||
const color = node ? getNodeColor(node.type) : "hsl(var(--lp-dot-default))";
|
||||
const title = node?.title || node?.type || "";
|
||||
|
||||
const icon = {
|
||||
@@ -107,6 +107,7 @@ function DiffItem({
|
||||
{icon}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
{/* arbitrary-value: badge font size */}
|
||||
<span className="text-[10px] px-1.5 py-0.5 rounded bg-surface-container-highest font-medium flex-shrink-0">
|
||||
{label}
|
||||
</span>
|
||||
|
||||
@@ -35,8 +35,8 @@ export function VersionDiffViewer({
|
||||
|
||||
const diffSegments = useMemo(() => {
|
||||
if (!selectedVersion) return [];
|
||||
const versionContent = selectedVersion.content as unknown as LessonPlanDocument;
|
||||
return computeDocumentDiff(versionContent, currentDoc);
|
||||
// LessonPlanVersion.content 已是 LessonPlanDocument 类型,无需断言
|
||||
return computeDocumentDiff(selectedVersion.content, currentDoc);
|
||||
}, [selectedVersion, currentDoc]);
|
||||
|
||||
const summary = useMemo(() => summarizeDiff(diffSegments), [diffSegments]);
|
||||
@@ -45,6 +45,7 @@ export function VersionDiffViewer({
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[60] flex items-center justify-center bg-black/40">
|
||||
{/* arbitrary-value: dialog fixed width */}
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
|
||||
@@ -51,13 +51,17 @@ export function isEmailEnabled(): boolean {
|
||||
function getTypeColor(type: NotificationPayload["type"]): string {
|
||||
switch (type) {
|
||||
case "success":
|
||||
// eslint-disable-next-line no-restricted-syntax -- email HTML inline style requires literal hex
|
||||
return "#16a34a"
|
||||
case "warning":
|
||||
// eslint-disable-next-line no-restricted-syntax -- email HTML inline style requires literal hex
|
||||
return "#d97706"
|
||||
case "error":
|
||||
// eslint-disable-next-line no-restricted-syntax -- email HTML inline style requires literal hex
|
||||
return "#dc2626"
|
||||
case "info":
|
||||
default:
|
||||
// eslint-disable-next-line no-restricted-syntax -- email HTML inline style requires literal hex
|
||||
return "#2563eb"
|
||||
}
|
||||
}
|
||||
@@ -65,6 +69,14 @@ function getTypeColor(type: NotificationPayload["type"]): string {
|
||||
/** 生成 HTML 邮件内容 */
|
||||
function buildHtmlContent(payload: NotificationPayload): string {
|
||||
const color = getTypeColor(payload.type)
|
||||
// eslint-disable-next-line no-restricted-syntax -- email HTML inline style requires literal hex
|
||||
const contentTextColor = "#374151"
|
||||
// eslint-disable-next-line no-restricted-syntax -- email HTML inline style requires literal hex
|
||||
const dividerColor = "#e5e7eb"
|
||||
// eslint-disable-next-line no-restricted-syntax -- email HTML inline style requires literal hex
|
||||
const footerTextColor = "#9ca3af"
|
||||
// arbitrary-value: email HTML inline style requires literal px
|
||||
const footerFontSizeDecl = "font-size:12px"
|
||||
const actionLink = payload.actionUrl
|
||||
? `<p style="margin-top:16px;"><a href="${payload.actionUrl}" style="color:${color};text-decoration:none;">点击查看详情 →</a></p>`
|
||||
: ""
|
||||
@@ -72,11 +84,11 @@ function buildHtmlContent(payload: NotificationPayload): string {
|
||||
<div style="font-family:Arial,sans-serif;max-width:600px;margin:0 auto;padding:20px;">
|
||||
<div style="border-left:4px solid ${color};padding-left:16px;">
|
||||
<h2 style="color:${color};margin:0 0 12px 0;">${escapeHtml(payload.title)}</h2>
|
||||
<p style="color:#374151;line-height:1.6;margin:0;">${escapeHtml(payload.content)}</p>
|
||||
<p style="color:${contentTextColor};line-height:1.6;margin:0;">${escapeHtml(payload.content)}</p>
|
||||
${actionLink}
|
||||
</div>
|
||||
<hr style="border:none;border-top:1px solid #e5e7eb;margin:24px 0;" />
|
||||
<p style="color:#9ca3af;font-size:12px;margin:0;">此邮件由系统自动发送,请勿回复。</p>
|
||||
<hr style="border:none;border-top:1px solid ${dividerColor};margin:24px 0;" />
|
||||
<p style="color:${footerTextColor};${footerFontSizeDecl};margin:0;">此邮件由系统自动发送,请勿回复。</p>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
@@ -235,7 +235,7 @@ export function ParentAttendanceCalendar({
|
||||
onFocus={() => setFocusedIdx(idx)}
|
||||
className={cn(
|
||||
"relative flex aspect-square flex-col items-center justify-center rounded-md border text-xs",
|
||||
"min-h-[36px] cursor-default",
|
||||
"min-h-9 cursor-default",
|
||||
record ? "border-border bg-muted/30" : "border-transparent",
|
||||
isToday && "ring-2 ring-primary ring-offset-1",
|
||||
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||
|
||||
@@ -35,17 +35,17 @@ import type {
|
||||
|
||||
/** 章节颜色调色板(柔和、Obsidian 风格) */
|
||||
const CHAPTER_COLORS = [
|
||||
"#7c3aed", "#2563eb", "#dc2626", "#059669",
|
||||
"#d97706", "#db2777", "#0891b2", "#65a30d",
|
||||
"#9333ea", "#0284c7", "#e11d48", "#16a34a",
|
||||
"hsl(var(--graph-node-1))", "hsl(var(--graph-node-2))", "hsl(var(--graph-node-3))", "hsl(var(--graph-node-4))",
|
||||
"hsl(var(--graph-node-5))", "hsl(var(--graph-node-6))", "hsl(var(--graph-node-1))", "hsl(var(--graph-node-2))",
|
||||
"hsl(var(--graph-node-3))", "hsl(var(--graph-node-4))", "hsl(var(--graph-node-5))", "hsl(var(--graph-node-6))",
|
||||
]
|
||||
|
||||
/** 掌握度色彩(柔和) */
|
||||
const MASTERY_FILL: Record<MasteryLevel, string> = {
|
||||
low: "#dc2626",
|
||||
medium: "#d97706",
|
||||
high: "#059669",
|
||||
unassessed: "#94a3b8",
|
||||
low: "hsl(var(--graph-node-1))",
|
||||
medium: "hsl(var(--graph-node-5))",
|
||||
high: "hsl(var(--graph-node-2))",
|
||||
unassessed: "hsl(var(--muted-foreground))",
|
||||
}
|
||||
|
||||
/** 边颜色:父子关系(中性灰)/ 前置依赖(紫色) */
|
||||
@@ -161,7 +161,7 @@ function buildGraphData(
|
||||
kp,
|
||||
mastery: data.masteryMap[kp.id] ?? null,
|
||||
val: connectionCount.get(kp.id) ?? 0,
|
||||
chapterColor: chapterColorMap.get(kp.chapterId ?? "") ?? "#6b7280",
|
||||
chapterColor: chapterColorMap.get(kp.chapterId ?? "") ?? "hsl(var(--muted-foreground))",
|
||||
}))
|
||||
|
||||
const links: KpGraphLink[] = []
|
||||
|
||||
@@ -54,8 +54,8 @@ const edgeTypes = { prerequisiteEdge: GraphPrerequisiteEdge }
|
||||
|
||||
/** 章节颜色调色板 */
|
||||
const CHAPTER_COLORS = [
|
||||
"#3b82f6", "#ef4444", "#10b981", "#f59e0b",
|
||||
"#8b5cf6", "#ec4899", "#06b6d4", "#84cc16",
|
||||
"hsl(var(--graph-node-1))", "hsl(var(--graph-node-2))", "hsl(var(--graph-node-3))", "hsl(var(--graph-node-4))",
|
||||
"hsl(var(--graph-node-5))", "hsl(var(--graph-node-6))", "hsl(var(--graph-node-1))", "hsl(var(--graph-node-2))",
|
||||
]
|
||||
|
||||
interface KnowledgeGraphProps {
|
||||
@@ -166,7 +166,7 @@ function KnowledgeGraphInner({ textbookId, initialViewMode = "structure", initia
|
||||
viewMode,
|
||||
isSelected,
|
||||
isHighlighted,
|
||||
chapterColor: chapterColorMap.get(kp.chapterId ?? "") ?? "#6b7280",
|
||||
chapterColor: chapterColorMap.get(kp.chapterId ?? "") ?? "hsl(var(--muted-foreground))",
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -330,7 +330,7 @@ function KnowledgeGraphInner({ textbookId, initialViewMode = "structure", initia
|
||||
// 安全的类型收窄:node.data 是 Record<string, unknown>,
|
||||
// GraphLayoutNodeData 有索引签名 [key: string]: unknown,是 Record 的子类型
|
||||
const data = node.data as GraphLayoutNodeData
|
||||
return data.graphData?.chapterColor ?? "#6b7280"
|
||||
return data.graphData?.chapterColor ?? "hsl(var(--muted-foreground))"
|
||||
}}
|
||||
/>
|
||||
</ReactFlow>
|
||||
|
||||
@@ -51,6 +51,7 @@ const ChartContainer = React.forwardRef<
|
||||
data-chart={chartId}
|
||||
ref={ref}
|
||||
className={cn(
|
||||
// eslint-disable-next-line no-restricted-syntax -- arbitrary-value: recharts CSS 属性选择器需匹配 SVG 默认输出 #ccc/#fff 不可令牌化
|
||||
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
|
||||
className
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user