/* 标博士 · UI demo 公共脚本:图标、页面框架、轻提示、对话框、下拉、日期、开关、标签页、排序。
* 只演示交互,不连后端。正式实现里这些交互件由 Reka UI 打底(AR10)。 */
(function () {
const $ = (s, el = document) => el.querySelector(s);
const $$ = (s, el = document) => [...el.querySelectorAll(s)];
const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
/* Lucide 图标(ISC 许可)路径 */
const ICONS = {
grid: '',
folder: '',
cert: '',
gauge: '',
bell: '',
help: '',
x: '',
plus: '',
upload: '',
download: '',
file: '',
search: '',
"chev-down": '',
"chev-left": '',
"chev-right": '',
sort: '',
"arrow-up": '',
"arrow-down": '',
calendar: '',
check: '',
more: '',
filter: '',
inbox: '',
clock: '',
trash: '',
pencil: '',
history: '',
keyboard: '',
refresh: '',
send: '',
list: '',
paperclip: '',
book: '',
archive: '',
layers: ''
};
const icon = (n, cls = "") => ``;
function hydrate(root = document) {
$$("i[data-ic]", root).forEach((el) => { el.outerHTML = icon(el.dataset.ic, el.className); });
}
/* ---------- demo 顶部导航(控件页) ---------- */
const PAGES = [
["index.html", "目录"], ["controls.html", "① 控件"], ["forms.html", "② 表单"], ["cards.html", "③ 卡片"],
["lists.html", "④ 列表与表格"], ["page-workbench.html", "⑤ 工作台"], ["page-reading.html", "⑥ 读标"]
];
function demoTop(cur) {
const h = $("[data-demo-top]");
if (!h) return;
h.className = "d-top";
h.innerHTML = `
UI demo
`;
}
/* ---------- 产品页面框架(组合页) ---------- */
function shell(opt) {
const nav = [["page-workbench.html", "grid", "工作台", "workbench"], ["#", "cert", "资质库", "quals"], ["#", "gauge", "额度", "quota"]]
.map(([h, i, t, k]) => `${icon(i, "lg")}${t}`).join("");
const crumbs = (opt.crumbs || []).map((c, i, a) => i < a.length - 1 ? `${c[0]}/` : `${c[0]}`).join("");
document.body.classList.add("app");
document.body.insertAdjacentHTML("afterbegin", ``);
const main = $("main");
main.classList.add("main");
main.insertAdjacentHTML("afterbegin", `${crumbs}
解析37检查12
`);
$("[data-notice]").onclick = () => toast("解析完成:补遗 1 已并入清单(新增 2 条,更新 1 条)。");
$("[data-keys]").onclick = keysDialog;
}
function keysDialog() {
dialog({ title: "快捷键", cancel: null, ok: "知道了",
body: `
| J / K | 下一条 / 上一条 |
| O 或 Enter | 在原文查看器里打开当前出处 |
| 1 2 3 | 确认无误 / 已修改 / 忽略(3 会打开原因输入框) |
| U | 撤销当前条的处理 |
| ? | 显示本说明 |
` });
}
/* ---------- 轻提示:4 秒消失,悬停暂停 ---------- */
function toast(msg, opt = {}) {
let box = $(".toasts");
if (!box) { box = document.createElement("div"); box.className = "toasts"; box.setAttribute("role", "status"); box.setAttribute("aria-live", "polite"); document.body.appendChild(box); }
const t = document.createElement("div");
t.className = "toast";
t.innerHTML = `${opt.ok === false ? "" : '✓'}${msg}${opt.undo ? '' : ""}`;
box.appendChild(t);
let left = 4000, start = Date.now(), timer = setTimeout(() => t.remove(), left);
t.addEventListener("mouseenter", () => { clearTimeout(timer); left -= Date.now() - start; });
t.addEventListener("mouseleave", () => { start = Date.now(); timer = setTimeout(() => t.remove(), Math.max(left, 1200)); });
if (opt.undo) $("[data-undo]", t).onclick = () => { t.remove(); opt.undo(); };
}
/* ---------- 对话框:Esc 关闭、焦点回到触发处 ---------- */
function dialog({ title, body, ok = "确定", cancel = "取消", wide = false, onOk, onOpen, footLeft = "" }) {
const back = document.activeElement;
const s = document.createElement("div");
s.className = "scrim";
s.innerHTML = `
${title}
${body}
`;
document.body.appendChild(s);
document.body.style.overflow = "hidden";
const close = () => { s.remove(); document.body.style.overflow = ""; document.removeEventListener("keydown", onKey, true); if (back && back.focus) back.focus(); };
const onKey = (e) => {
if (e.key === "Escape") { e.stopPropagation(); close(); }
if (e.key === "Tab") { // 焦点锁定在对话框内
const f = $$("button:not([disabled]), input:not([disabled]), textarea, select, a[href]", s);
if (!f.length) return;
if (e.shiftKey && document.activeElement === f[0]) { e.preventDefault(); f[f.length - 1].focus(); }
else if (!e.shiftKey && document.activeElement === f[f.length - 1]) { e.preventDefault(); f[0].focus(); }
}
};
document.addEventListener("keydown", onKey, true);
s.addEventListener("mousedown", (e) => { if (e.target === s) close(); });
$("[data-x]", s).onclick = close;
if (cancel) $("[data-cancel]", s).onclick = close;
$("[data-ok]", s).onclick = () => { if (!onOk || onOk(s) !== false) close(); };
hydrate(s); init(s);
if (onOpen) onOpen(s);
const f = $("textarea, input:not([type=checkbox]):not([type=radio]), [data-ok]", s);
if (f) f.focus();
return s;
}
/* ---------- 下拉选择(listbox)与下拉菜单 ---------- */
function closePops(except) {
$$(".pop.open").forEach((p) => { if (p !== except) { p.classList.remove("open"); const t = $("[aria-expanded]", p); if (t) t.setAttribute("aria-expanded", "false"); const m = $(".menu,.cal", p); if (m) m.hidden = true; } });
}
function initSelect(p) {
const trg = $(".select, [data-menu-trigger]", p), menu = $(".menu", p);
if (!trg || !menu || p.dataset.ready) return;
p.dataset.ready = 1;
menu.hidden = true;
const opts = () => $$('[role=option]:not([aria-disabled="true"]), [role=menuitem]:not([aria-disabled="true"])', menu);
let ai = -1;
const mark = (i) => { opts().forEach((o, k) => o.classList.toggle("is-active", k === i)); ai = i; const o = opts()[i]; if (o) o.scrollIntoView({ block: "nearest" }); };
const open = () => { closePops(p); p.classList.add("open"); menu.hidden = false; trg.setAttribute("aria-expanded", "true"); const sel = opts().findIndex((o) => o.getAttribute("aria-selected") === "true"); mark(sel < 0 ? 0 : sel); };
const close = () => { p.classList.remove("open"); menu.hidden = true; trg.setAttribute("aria-expanded", "false"); };
const pick = (o) => {
if (o.getAttribute("role") === "option") {
$$("[role=option]", menu).forEach((x) => x.setAttribute("aria-selected", x === o ? "true" : "false"));
const v = $(".val", trg); if (v) v.innerHTML = o.dataset.label || o.firstChild.textContent;
p.closest(".field")?.classList.remove("err");
p.dispatchEvent(new CustomEvent("change", { detail: o.dataset.value || o.textContent }));
} else if (o.dataset.toast) toast(o.dataset.toast);
close(); trg.focus();
};
trg.addEventListener("click", (e) => { e.stopPropagation(); p.classList.contains("open") ? close() : open(); });
trg.addEventListener("keydown", (e) => {
const n = opts().length;
if (["ArrowDown", "ArrowUp"].includes(e.key)) { e.preventDefault(); if (!p.classList.contains("open")) return open(); mark((ai + (e.key === "ArrowDown" ? 1 : n - 1)) % n); }
else if (e.key === "Enter" || e.key === " ") { if (p.classList.contains("open") && ai >= 0) { e.preventDefault(); pick(opts()[ai]); } }
else if (e.key === "Escape" && p.classList.contains("open")) { e.stopPropagation(); close(); }
});
menu.addEventListener("click", (e) => { const o = e.target.closest('[role=option], [role=menuitem]'); if (o && o.getAttribute("aria-disabled") !== "true") { e.stopPropagation(); pick(o); } });
}
/* ---------- 日期选择 ---------- */
const WD = ["一", "二", "三", "四", "五", "六", "日"];
function calHTML(y, m, sel, opt = {}) {
const first = new Date(y, m, 1), start = (first.getDay() + 6) % 7, days = new Date(y, m + 1, 0).getDate();
const prevDays = new Date(y, m, 0).getDate();
let cells = "";
for (let i = 0; i < 42; i++) {
const d = i - start + 1;
let dd, out = false, mm = m;
if (d < 1) { dd = prevDays + d; out = true; mm = m - 1; } else if (d > days) { dd = d - days; out = true; mm = m + 1; } else dd = d;
const date = new Date(y, mm, dd), key = `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`;
const cls = [out ? "out" : "", key === opt.today ? "today" : "", (opt.marks || []).includes(key) ? "mark" : ""].join(" ");
cells += ``;
if (i === 34 && d >= days) break;
}
return `${y} 年 ${m + 1} 月
${WD.map((w) => `${w}`).join("")}${cells}
${opt.time ? `时间 ` : '点圆点的日期是本项目的关键日期'}
`;
}
function initDate(p) {
if (p.dataset.ready) return;
p.dataset.ready = 1;
const trg = $(".select", p), cal = $(".cal", p);
let [y, m] = (p.dataset.month || "2026-11").split("-").map(Number); m -= 1;
let sel = p.dataset.value || "";
const opt = { today: "2026-11-3", marks: (p.dataset.marks || "").split(",").filter(Boolean), time: p.dataset.time, min: p.dataset.min ? new Date(...p.dataset.min.split("-").map((v, i) => i === 1 ? v - 1 : +v)) : null };
const render = () => { cal.innerHTML = calHTML(y, m, sel, opt); };
const fmt = (k) => { const [a, b, c] = k.split("-"); return `${a}-${String(b).padStart(2, "0")}-${String(c).padStart(2, "0")}`; };
render();
if (!p.classList.contains("d-open")) cal.hidden = true;
trg?.addEventListener("click", (e) => { e.stopPropagation(); const o = !p.classList.contains("open"); closePops(p); p.classList.toggle("open", o); cal.hidden = !o; trg.setAttribute("aria-expanded", o); });
cal.addEventListener("click", (e) => {
e.stopPropagation();
const b = e.target.closest("button"); if (!b) return;
if (b.dataset.cm) { m += +b.dataset.cm; if (m < 0) { m = 11; y--; } if (m > 11) { m = 0; y++; } render(); return; }
if (b.dataset.today) { y = 2026; m = 10; sel = opt.today; }
else if (b.dataset.d) sel = b.dataset.d;
else return;
render();
if (trg) { $(".val", trg).innerHTML = fmt(sel); p.closest(".field")?.classList.remove("err"); if (!p.classList.contains("d-open")) { p.classList.remove("open"); cal.hidden = true; trg.setAttribute("aria-expanded", "false"); trg.focus(); } }
p.dispatchEvent(new CustomEvent("change", { detail: fmt(sel) }));
});
}
/* ---------- 开关、分段、标签页、半选、排序、加载按钮 ---------- */
function init(root = document) {
$$(".pop[data-select], .pop[data-menu]", root).forEach(initSelect);
$$(".pop[data-date]", root).forEach(initDate);
$$(".switch:not([data-ready])", root).forEach((s) => {
s.dataset.ready = 1;
const upd = () => { const st = $(".st", s); if (st) st.textContent = s.getAttribute("aria-checked") === "true" ? "开" : "关"; };
upd();
s.addEventListener("click", () => { if (s.disabled) return; s.setAttribute("aria-checked", s.getAttribute("aria-checked") === "true" ? "false" : "true"); upd(); });
});
$$(".seg:not([data-ready])", root).forEach((g) => {
g.dataset.ready = 1;
g.addEventListener("click", (e) => { const b = e.target.closest("button"); if (!b) return; $$("button", g).forEach((x) => x.setAttribute("aria-pressed", x === b)); g.dispatchEvent(new CustomEvent("change", { detail: b.dataset.v })); });
});
$$("[role=tablist]:not([data-ready])", root).forEach((tl) => {
tl.dataset.ready = 1;
const tabs = $$("[role=tab]", tl);
const sel = (t) => { tabs.forEach((x) => { const on = x === t; x.setAttribute("aria-selected", on); x.tabIndex = on ? 0 : -1; const pn = x.getAttribute("aria-controls"); if (pn) document.getElementById(pn).hidden = !on; }); };
tabs.forEach((t) => t.addEventListener("click", () => { if (t.getAttribute("aria-disabled") !== "true") sel(t); }));
tl.addEventListener("keydown", (e) => {
const i = tabs.indexOf(document.activeElement); if (i < 0) return;
const d = e.key === "ArrowRight" ? 1 : e.key === "ArrowLeft" ? -1 : 0; if (!d) return;
e.preventDefault(); let j = i; do { j = (j + d + tabs.length) % tabs.length; } while (tabs[j].getAttribute("aria-disabled") === "true");
tabs[j].focus(); sel(tabs[j]);
});
});
$$("input[data-indeterminate]", root).forEach((c) => { c.indeterminate = true; });
$$("[data-loading]:not([data-ready])", root).forEach((b) => {
b.dataset.ready = 1;
b.addEventListener("click", () => {
if (b.classList.contains("is-loading")) return;
b.classList.add("is-loading"); b.setAttribute("aria-busy", "true");
setTimeout(() => { b.classList.remove("is-loading"); b.removeAttribute("aria-busy"); if (b.dataset.loading) toast(b.dataset.loading); }, 2200);
});
});
}
document.addEventListener("click", () => closePops());
document.addEventListener("keydown", (e) => { if (e.key === "Escape") closePops(); });
/* 可排序表格:th[data-sort] 里放 button.th-sort;按 data-sort 指定的列取 td 的 data-v 或文字 */
function sortable(table, onSort) {
$$("th[data-sort]", table).forEach((th) => {
const b = $(".th-sort", th);
b.insertAdjacentHTML("beforeend", icon("sort"));
b.addEventListener("click", () => {
const dir = th.getAttribute("aria-sort") === "ascending" ? "descending" : "ascending";
$$("th[aria-sort]", table).forEach((x) => { x.removeAttribute("aria-sort"); $("svg", x).outerHTML = icon("sort"); });
th.setAttribute("aria-sort", dir);
$("svg", th).outerHTML = icon(dir === "ascending" ? "arrow-up" : "arrow-down");
onSort(th.dataset.sort, dir);
});
});
}
window.Demo = { $, $$, esc, icon, hydrate, demoTop, shell, toast, dialog, init, sortable, keysDialog, calHTML };
document.addEventListener("DOMContentLoaded", () => { hydrate(); init(); });
})();