+
+ e-Court Documents Versioning
+ {phase !== "signed-out" && phase !== "checking" && username ? (
+ /* จอแคบ: username ย้ายไป title/aria-label — ไม่กินพื้นที่แถบหัวจนตัวชื่อแอป wrap 3-4 บรรทัด */
+
+ ) : null}
+
+
+ {phase === "checking" ? (
+
+
+ {bootError ? (
+ void detect()}
+ >
+ ลองใหม่
+
+ }
+ >
+ {bootError}
+
+ ) : null}
+
+ ) : null}
+
+ {phase === "signed-out" ? (
+
+
เข้าสู่ระบบเพื่อเริ่มใช้งาน versioning
+
+ ใช้บัญชีของ realm word-versioning — หน้าต่าง login จะเปิดแยกต่างหาก
+
+
+ {loginError ?
{loginError} : null}
+
+ ) : null}
+
+ {phase === "untracked" ? (
+
+
เอกสารนี้ยังไม่ได้ติดตาม version
+
+ กดปุ่มด้านล่างเพื่อเริ่ม track — ระบบจะ stamp metadata ลงไฟล์และอัปโหลดเป็น version 1
+ (ไฟล์ที่สร้างจากเว็บ portal จะถูกตรวจพบและข้ามขั้นตอนนี้อัตโนมัติ)
+
+ {documentTemplateId ? (
+
+ ไฟล์นี้เป็นไฟล์ร่างจาก template — กดเริ่มติดตามแล้วระบบจะผูกเอกสารกับ template ต้นทาง
+ และ sync ค่า fields ให้อัตโนมัติ
+
+ ) : null}
+
+
void handleSaveOnly()}
+ onSaveAndClose={() => void handleSaveAndClose()}
+ />
+ {starting && progressMessage ? : null}
+ {flowError ? {flowError} : null}
+
+ ) : null}
+
+ {phase === "tracking" ? (
+ /* ห้ามใส่ items-start — ลูกทุกตัวจะหดตามเนื้อหา (MessageBar/history list ไม่เต็มความกว้าง pane) */
+
+ {/* กลุ่มข้อความสถานะ */}
+ {hasUncommitted ? (
+
+ มีการแก้ไขที่ยังไม่ถูกบันทึกเป็นเวอร์ชันใหม่ — กด Save & Version เพื่อบันทึกเป็น version ใหม่
+
+ ) : null}
+
+ {flowWarning ?
{flowWarning} : null}
+ {flowSuccess ?
{flowSuccess} : null}
+ {flowError ?
{flowError} : null}
+
+ {/* กลุ่ม action บันทึก — ปุ่มหลักเต็มความกว้างแถบ (action เด่นที่สุดของทั้งแอป) + progress + local save
+ icon ใช้ commit ไม่ใช่ floppy — แยกจากปุ่ม "บันทึก" (local save) ที่ความหมายต่างกัน */}
+
+
+ {saving && progressMessage ? : null}
+ void handleSaveOnly()}
+ onSaveAndClose={() => void handleSaveAndClose()}
+ />
+
+
+ {/* ink-soft (ไม่ใช่ ink-faint 2.6:1) — tracking id คือ identifier ที่ผู้ใช้อาจต้องอ่าน/คัดลอกตอนแจ้งปัญหา */}
+
tracking id: {documentId?.slice(0, 8)}…
+
+
void handleDownload(version, "pdf")}
+ onDownload={(version) => void handleDownload(version)}
+ onRetry={() => documentId && void refreshHistory(documentId)}
+ />
+
+ ) : null}
+
+ {/* กดยืนยันแล้วปิด dialog ทันที — progress/ผลลัพธ์แสดงที่ pane (dialog ค้าง = บังผลลัพธ์
+ + เสี่ยงกดยืนยันซ้ำสร้าง version ซ้ำ; busy ใน dialog เหลือเป็น defense ชั่วขณะก่อน re-render) */}
+
{
+ setConfirmOpen(false);
+ void handleSaveAndVersion(message);
+ }}
+ {...(closeSupported
+ ? {
+ onConfirmClose: (message: string) => {
+ setConfirmOpen(false);
+ void handleSaveAndVersion(message, true);
+ },
+ }
+ : {})}
+ onDismiss={() => setConfirmOpen(false)}
+ />
+
+ {/* FR-13 — Start Versioning ใช้ dialog เดียวกัน: ยืนยัน + พิมพ์ commit message ของ v1 ก่อนเริ่ม track */}
+ {
+ setStartConfirmOpen(false);
+ void handleStartVersioning(message);
+ }}
+ onDismiss={() => setStartConfirmOpen(false)}
+ />
+
+ );
+};
+
+export default TaskpaneApp;
diff --git a/components/wordaddin/VersionHistoryPanel.tsx b/components/wordaddin/VersionHistoryPanel.tsx
new file mode 100644
index 0000000..7755035
--- /dev/null
+++ b/components/wordaddin/VersionHistoryPanel.tsx
@@ -0,0 +1,149 @@
+import { Download, FileText, LoaderCircle } from "lucide-react";
+
+import { VersionRecord } from "@/wordaddin/models/versionRecord";
+
+import { MessageBar } from "./MessageBar";
+import { Spinner } from "./Spinner";
+
+/**
+ * FR-4 — แสดง version history (เลข version / เวลา / ผู้สร้าง) + ปุ่มดาวน์โหลด version นั้น
+ * ชั้นภาพ: timeline แนวตั้ง — จุดโหนดเท่าทึบคือเวอร์ชันล่าสุด จุดกลวงคือเก่ากว่า
+ * (เลข version เป็น static → ใช้ ink ตามกฎ "blue เฉพาะ interactive" ของ DESIGN.md)
+ */
+
+function formatTimestamp(iso: string): string {
+ const date = new Date(iso);
+ return Number.isNaN(date.getTime())
+ ? iso
+ : date.toLocaleString(undefined, { dateStyle: "medium", timeStyle: "short" });
+}
+
+/** px-2 py-1.5 + text-xs ≈ สูง 30px — พ้นเกณฑ์ touch target 24px ของ WCAG 2.5.8 โดยยัง dense พอสำหรับ list */
+const BTN_ACTION =
+ "focus-ring inline-flex items-center gap-1 rounded-md px-2 py-1.5 text-xs text-ink-soft transition hover:bg-primary/[0.06] hover:text-primary disabled:pointer-events-none disabled:opacity-50";
+
+export function VersionHistoryPanel({
+ versions,
+ loading,
+ error,
+ downloadingVersionId,
+ downloadingPdfVersionId,
+ onDownloadPdf,
+ onDownload,
+ onRetry,
+}: {
+ versions: VersionRecord[];
+ loading: boolean;
+ error: string | null;
+ /** versionId ของ version ที่กำลังดาวน์โหลดอยู่ (แสดง spinner ที่ปุ่ม) */
+ downloadingVersionId: string | null;
+ downloadingPdfVersionId: string | null;
+ onDownloadPdf: (version: VersionRecord) => void;
+ onDownload: (version: VersionRecord) => void;
+ onRetry: () => void;
+}) {
+ const anyDownloading = downloadingVersionId !== null || downloadingPdfVersionId !== null;
+
+ return (
+
+
+
Version history
+ {versions.length > 0 ? {versions.length} เวอร์ชัน : null}
+
+
+ {loading ?
: null}
+
+ {error ? (
+
+ ลองใหม่
+
+ }
+ >
+ {error}
+
+ ) : null}
+
+ {!loading && !error && versions.length === 0 ? (
+
ยังไม่มี version ในระบบ
+ ) : null}
+
+ {versions.length > 0 ? (
+
+ {/* ราง timeline — วาดครั้งเดียวระดับรายการ (เวอร์ชันเดียวไม่ต้องมีเส้น) */}
+ {versions.length > 1 ? (
+
+ ) : null}
+ {versions.map((version, index) => {
+ const isLatest = index === 0;
+ return (
+ -
+
+
+ v{version.versionNumber}
+ {isLatest ? (
+ ล่าสุด
+ ) : null}
+
+
+ {version.hasPdf ? (
+
+ ) : null}
+
+
+ {version.message ? (
+
+ {version.message}
+
+ ) : null}
+
+ {formatTimestamp(version.createdAt)} · โดย {version.createdByUserId}
+
+
+ );
+ })}
+
+ ) : null}
+
+ );
+}
+
+export default VersionHistoryPanel;
diff --git a/components/wordaddin/WordAddinGate.tsx b/components/wordaddin/WordAddinGate.tsx
new file mode 100644
index 0000000..5df16c3
--- /dev/null
+++ b/components/wordaddin/WordAddinGate.tsx
@@ -0,0 +1,131 @@
+"use client";
+
+import { useEffect, useState } from "react";
+
+import { withBasePath } from "@/utils/base-path";
+import { signInForDemo } from "@/wordaddin/services/authService";
+
+import { MessageBar } from "./MessageBar";
+import { Spinner } from "./Spinner";
+import TaskpaneApp from "./TaskpaneApp";
+
+/**
+ * Gate ของ taskpane ใน App Router — ทำหน้าที่แทน Office.onReady ก่อน render ของ webpack
+ * ต้นฉบับ: Office.js โหลดใน ของ taskpane.html แล้ว Office.onReady ก่อน createRoot
+ * (บริบท Next: ฉีด