feat(lesson-preparation): add anchor canvas design, new blocks, and textbook content node

- Add anchor injector for canvas-based anchor positioning

- Add new block components: blackboard, homework, import, key-point, new-teaching, objective, summary

- Add textbook content node for React Flow canvas

- Update actions (kp, publish, main), data-access (templates, versions, main)

- Update editor, node-editor, block-renderer, and picker components

- Update schema, types, hooks, and lib utilities (document-migration, node-summary, rf-mappers)
This commit is contained in:
SpecialX
2026-06-23 17:37:19 +08:00
parent 1fcef5c3aa
commit 2197e68069
34 changed files with 3190 additions and 402 deletions

View File

@@ -0,0 +1,78 @@
"use client";
import { useTranslations } from "next-intl";
import { Plus, Trash2 } from "lucide-react";
import type { SummaryBlockData } from "../../types";
import { Button } from "@/shared/components/ui/button";
interface Props {
data: SummaryBlockData;
onUpdate: (data: SummaryBlockData) => void;
}
export function SummaryBlock({ data, onUpdate }: Props) {
const t = useTranslations("lessonPreparation");
function updatePoint(index: number, value: string) {
const next = data.summaryPoints.map((p, i) => (i === index ? value : p));
onUpdate({ ...data, summaryPoints: next });
}
function addPoint() {
onUpdate({
...data,
summaryPoints: [...data.summaryPoints, ""],
});
}
function removePoint(index: number) {
onUpdate({
...data,
summaryPoints: data.summaryPoints.filter((_, i) => i !== index),
});
}
return (
<div className="space-y-2">
<div className="text-xs text-on-surface-variant">
{t("summary.hint")}
</div>
{data.summaryPoints.map((point, idx) => (
<div key={idx} className="flex items-start gap-2">
<span className="text-xs text-on-surface-variant mt-1">{idx + 1}.</span>
<input
type="text"
value={point}
onChange={(e) => updatePoint(idx, e.target.value)}
className="flex-1 text-sm border border-outline-variant rounded px-2 py-1"
placeholder={t("summary.pointPlaceholder")}
/>
<Button
variant="ghost"
size="sm"
className="!p-1 text-error"
onClick={() => removePoint(idx)}
aria-label={t("action.delete")}
>
<Trash2 className="w-3 h-3" />
</Button>
</div>
))}
<Button variant="outline" size="sm" onClick={addPoint}>
<Plus className="w-3 h-3 mr-1" />
{t("summary.addPoint")}
</Button>
<div>
<label className="text-xs font-medium block mb-1 mt-2">
{t("summary.homeworkPreviewLabel")}
</label>
<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]"
placeholder={t("summary.homeworkPreviewPlaceholder")}
/>
</div>
</div>
);
}