/* 共用元件 — components.jsx */
/* ---------- 圖示(簡潔線性 icon) ---------- */
function Icon({ name, size = 18, stroke = 2 }) {
const common = {
width: size, height: size, viewBox: "0 0 24 24", fill: "none",
stroke: "currentColor", strokeWidth: stroke, strokeLinecap: "round",
strokeLinejoin: "round"
};
const paths = {
chart: <>>,
user: <>>,
pencil: <>>,
upload: <>>,
spark: <>>,
users: <>>,
plus: <>>,
trash: <>>,
check: ,
chevron: ,
logout: <>>,
search: <>>,
file: <>>,
sheet: <>>,
calendar: <>>,
arrowUp: <>>,
arrowDown: <>>,
info: <>>,
lock: <>>,
mail: <>>,
award: <>>
};
return ;
}
/* ---------- 卡片 ---------- */
function Card({ children, style, pad = true, className }) {
return (
{children}
);
}
/* ---------- 統計卡 ---------- */
function StatCard({ icon, label, value, suffix, tone, hint }) {
return (
{label}
{icon && }
{value}{suffix && {suffix}}
{hint && {hint}
}
);
}
/* ---------- 評量類型徽章 ---------- */
const TYPE_COLORS = {
"作業": "type-a", "小考": "type-b", "課堂表現": "type-c", "分組活動": "type-d"
};
function TypeBadge({ type }) {
return {type};
}
/* ---------- 成績色塊(紅黃綠) ---------- */
function ScoreChip({ score, size = "md" }) {
const lvl = window.APPDATA.gradeLevel(score);
return (
{score}
);
}
/* ---------- 風險燈號 ---------- */
function RiskLight({ level, size = 14 }) {
return ;
}
/* ---------- 按鈕 ---------- */
function Button({ children, onClick, variant = "primary", icon, size, disabled, type, full }) {
return (
);
}
/* ---------- 區段標題 ---------- */
function SectionTitle({ icon, children, right }) {
return (
{icon && }
{children}
{right &&
{right}
}
);
}
/* ---------- 篩選 chip ---------- */
function FilterChip({ active, onClick, children }) {
return (
);
}
/* ---------- 頭像 ---------- */
function Avatar({ name, size = 36 }) {
const ch = (name || "?").slice(0, 1);
return (
{ch}
);
}
/* ---------- 小型長條分佈圖 ---------- */
function DistBar({ buckets }) {
const max = Math.max(1, ...buckets.map(b => b.count));
return (
{buckets.map((b, i) => (
))}
);
}
/* ---------- 折線(學期趨勢,簡易 SVG) ---------- */
function TrendLine({ points, height = 90 }) {
if (!points.length) return null;
const w = 100, h = 100, pad = 8;
const xs = points.map((_, i) => pad + (i / Math.max(1, points.length - 1)) * (w - pad * 2));
const ys = points.map(p => h - pad - (p / 100) * (h - pad * 2));
const d = xs.map((x, i) => (i === 0 ? "M" : "L") + x.toFixed(1) + " " + ys[i].toFixed(1)).join(" ");
const area = d + " L" + xs[xs.length - 1].toFixed(1) + " " + (h - pad) + " L" + xs[0].toFixed(1) + " " + (h - pad) + " Z";
return (
);
}
Object.assign(window, {
Icon, Card, StatCard, TypeBadge, ScoreChip, RiskLight, Button,
SectionTitle, FilterChip, Avatar, DistBar, TrendLine, TYPE_COLORS
});