diff --git a/docs/compatibility.md b/docs/compatibility.md
index 933caeb..3b99c77 100644
--- a/docs/compatibility.md
+++ b/docs/compatibility.md
@@ -144,7 +144,7 @@ Rust integration tests reuse the repository's existing files under `tests/data/x
- Borrowed dynamic, typed, and structured readers; repeated metadata reads; callback stopping/errors; borrowed dynamic/schema/typed/multi-sheet writers.
- Structured formula text, cached values, A1 addresses, style IDs, built-in/custom number formats, ranges, and early iterator drop.
-Writer tests generate temporary workbooks through `MiniExcel::save_as*()` and read them back through `MiniExcel::query*()`, covering dynamic and typed values, dates, multiple worksheets, visible/hidden/very-hidden states, active-sheet selection, row counts, empty schemas, default/custom/disabled freeze panes, header/headerless/typed AutoFilter ranges, right-to-left views, bounded fixed AutoWidth output, explicit/hidden column layout, ordinary body wrapping with formatted-value exclusions, body alignment composed with wrapping and number formats, default/custom header styles, default/minimal cell style modes, explicit path overwrite behavior, and worksheet-name validation. Template tests cover scalar and mixed text, native numbers and booleans, XML escaping, formula-injection protection, missing-variable policy, empty and populated arrays, multiple sheets, style retention, path overwrite, and byte workflows. The WASM adapter has native unit tests, while Browser Lab Playwright tests cover generated-workbook rendering, query controls, inclusive end ranges, the collapsible and resizable control rail with its validated persisted layout, and desktop/mobile viewports.
+Writer tests generate temporary workbooks through `MiniExcel::save_as*()` and read them back through `MiniExcel::query*()`, covering dynamic and typed values, dates, multiple worksheets, visible/hidden/very-hidden states, active-sheet selection, row counts, empty schemas, default/custom/disabled freeze panes, header/headerless/typed AutoFilter ranges, right-to-left views, bounded fixed AutoWidth output, explicit/hidden column layout, ordinary body wrapping with formatted-value exclusions, body alignment composed with wrapping and number formats, default/custom header styles, default/minimal cell style modes, explicit path overwrite behavior, and worksheet-name validation. Template tests cover scalar and mixed text, native numbers and booleans, XML escaping, formula-injection protection, missing-variable policy, empty and populated arrays, multiple sheets, style retention, path overwrite, and byte workflows. The WASM adapter has native unit tests, while Browser Lab Playwright tests cover generated-workbook rendering, query controls, inclusive end ranges, and desktop/mobile viewports.
`TableStyle` controls ordinary cell formats and is not an OOXML table abstraction. Neither mode creates `xl/tables` entries or worksheet `tableParts`.
diff --git a/docs/compatibility.zh-CN.md b/docs/compatibility.zh-CN.md
index c71abc8..8014410 100644
--- a/docs/compatibility.zh-CN.md
+++ b/docs/compatibility.zh-CN.md
@@ -144,7 +144,7 @@ Rust integration test 复用仓库 `tests/data/xlsx` 下的现有文件,包括
- 借用动态/类型化/structured reader、重复 metadata 读取、callback 停止/error,以及借用动态/schema/类型化/多表 writer。
- structured formula text、缓存值、A1 地址、style ID、内置/自定义 number format、range 和提前丢弃迭代器。
-Writer test 通过 `MiniExcel::save_as*()` 生成临时 workbook,并使用 `MiniExcel::query*()` 回读,覆盖动态和类型化 value、date、多工作表、visible/hidden/very-hidden 状态、active sheet 选择、行数、空 schema、默认/自定义/禁用冻结窗格、header/headerless/typed AutoFilter 范围、从右到左 view、有界固定 AutoWidth 输出、explicit/hidden column layout、普通 body 换行及 formatted-value 排除、body 对齐与换行/number format 组合、默认/自定义 header style、默认/最小 cell style 模式、显式 path 覆盖行为和 worksheet name 验证。模板测试覆盖标量与混合文本、原生 number/boolean、XML 转义、公式注入防护、缺失变量策略、空数组与非空数组、多工作表、样式保留、path 覆盖和 byte 工作流。WASM adapter 有原生 unit test,Browser Lab Playwright test 则覆盖生成 workbook 的渲染、query 控件、包含端点的结束 range、可折叠、可拖曳调整宽度且会校验保存布局的控制栏,以及桌面/移动 viewport。
+Writer test 通过 `MiniExcel::save_as*()` 生成临时 workbook,并使用 `MiniExcel::query*()` 回读,覆盖动态和类型化 value、date、多工作表、visible/hidden/very-hidden 状态、active sheet 选择、行数、空 schema、默认/自定义/禁用冻结窗格、header/headerless/typed AutoFilter 范围、从右到左 view、有界固定 AutoWidth 输出、explicit/hidden column layout、普通 body 换行及 formatted-value 排除、body 对齐与换行/number format 组合、默认/自定义 header style、默认/最小 cell style 模式、显式 path 覆盖行为和 worksheet name 验证。模板测试覆盖标量与混合文本、原生 number/boolean、XML 转义、公式注入防护、缺失变量策略、空数组与非空数组、多工作表、样式保留、path 覆盖和 byte 工作流。WASM adapter 有原生 unit test,Browser Lab Playwright test 则覆盖生成 workbook 的渲染、query 控件、包含端点的结束 range,以及桌面/移动 viewport。
`TableStyle` 控制普通 cell format,并不是 OOXML table 抽象。两种模式都不会创建 `xl/tables` entry 或 worksheet `tableParts`。
diff --git a/web-demo/playwright.config.mjs b/web-demo/playwright.config.mjs
index e7b3da3..eb20e77 100644
--- a/web-demo/playwright.config.mjs
+++ b/web-demo/playwright.config.mjs
@@ -19,9 +19,5 @@ export default defineConfig({
projects: [
{ name: "desktop", use: { ...devices["Desktop Chrome"] } },
{ name: "mobile", use: { ...devices["Pixel 7"] } },
- {
- name: "mobile-narrow",
- use: { ...devices["Pixel 7"], viewport: { width: 320, height: 800 } },
- },
],
});
diff --git a/web-demo/public/app.js b/web-demo/public/app.js
index 609b397..93210b9 100644
--- a/web-demo/public/app.js
+++ b/web-demo/public/app.js
@@ -1,10 +1,4 @@
const MAX_FILE_SIZE = 64 * 1024 * 1024;
-const LAYOUT_STORAGE_KEY = "miniexcel.browser-lab.layout/v1";
-const RAIL_WIDTH_DEFAULT = 380;
-const RAIL_WIDTH_MIN = 260;
-const RAIL_WIDTH_MAX = 720;
-const RAIL_WIDTH_RATIO = 0.6;
-const RAIL_KEYBOARD_STEP = 16;
const worker = new Worker(new URL("./analysis-worker.js", import.meta.url), { type: "module" });
let nextRequestId = 1;
const pendingRequests = new Map();
@@ -23,14 +17,11 @@ const state = {
columnTypes: new Map(),
builderInitialized: false,
toastTimer: null,
- railWidth: RAIL_WIDTH_DEFAULT,
- railCollapsed: false,
};
const elements = Object.fromEntries(
[
- "runtimeStatus", "workspace", "controlRail", "railSplitter", "railToggleButton", "fileInput",
- "openFileButton", "loadDemoButton", "downloadDemoButton",
+ "runtimeStatus", "fileInput", "openFileButton", "loadDemoButton", "downloadDemoButton",
"downloadJsonButton", "downloadChunksButton", "downloadMarkdownChunksButton",
"downloadManifestButton", "downloadMarkdownButton", "dropZone",
"fileName", "fileSize", "sheetCount", "sheetSelect", "startCellInput", "endCellInput",
@@ -60,7 +51,6 @@ worker.addEventListener("error", (event) => {
pendingRequests.clear();
});
-initLayout();
bindEvents();
boot();
@@ -140,120 +130,6 @@ function bindEvents() {
elements.previewTab.addEventListener("click", () => setTab("grid"));
elements.jsonTab.addEventListener("click", () => setTab("json"));
elements.markdownTab.addEventListener("click", () => setTab("markdown"));
- elements.railToggleButton.addEventListener("click", () => setRailCollapsed(!state.railCollapsed));
- bindRailSplitter();
- window.addEventListener("resize", applyRailWidth);
-}
-
-function initLayout() {
- const stored = readStoredLayout();
- state.railWidth = stored.width;
- setRailCollapsed(stored.collapsed, { persist: false });
-}
-
-function readStoredLayout() {
- const fallback = { width: RAIL_WIDTH_DEFAULT, collapsed: false };
- try {
- const raw = window.localStorage.getItem(LAYOUT_STORAGE_KEY);
- if (!raw) return fallback;
- const parsed = JSON.parse(raw);
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return fallback;
- const { width, collapsed } = parsed;
- if (!Number.isFinite(width) || width < RAIL_WIDTH_MIN || width > RAIL_WIDTH_MAX) return fallback;
- if (typeof collapsed !== "boolean") return fallback;
- return { width, collapsed };
- } catch {
- return fallback;
- }
-}
-
-function persistLayout() {
- try {
- window.localStorage.setItem(
- LAYOUT_STORAGE_KEY,
- JSON.stringify({ width: Math.round(state.railWidth), collapsed: state.railCollapsed }),
- );
- } catch {
- // Storage can be unavailable (private browsing); the layout then only lasts for this session.
- }
-}
-
-function railWidthBounds() {
- const available = elements.workspace.clientWidth || window.innerWidth;
- const scaled = Math.round(available * RAIL_WIDTH_RATIO);
- return { min: RAIL_WIDTH_MIN, max: Math.max(RAIL_WIDTH_MIN, Math.min(RAIL_WIDTH_MAX, scaled)) };
-}
-
-function applyRailWidth() {
- const { min, max } = railWidthBounds();
- const width = Math.min(Math.max(Math.round(state.railWidth), min), max);
- elements.workspace.style.setProperty("--rail-width", `${width}px`);
- elements.railSplitter.setAttribute("aria-valuemin", String(min));
- elements.railSplitter.setAttribute("aria-valuemax", String(max));
- elements.railSplitter.setAttribute("aria-valuenow", String(width));
-}
-
-function setRailWidth(width, { persist = true } = {}) {
- const { min, max } = railWidthBounds();
- state.railWidth = Math.min(Math.max(Math.round(width), min), max);
- applyRailWidth();
- if (persist) persistLayout();
-}
-
-function setRailCollapsed(collapsed, { persist = true } = {}) {
- state.railCollapsed = collapsed;
- elements.controlRail.hidden = collapsed;
- elements.railSplitter.hidden = collapsed;
- elements.workspace.classList.toggle("is-collapsed", collapsed);
- elements.railToggleButton.setAttribute("aria-expanded", String(!collapsed));
- elements.railToggleButton.title = collapsed ? "Show workbook controls" : "Hide workbook controls";
- if (!collapsed) applyRailWidth();
- if (persist) persistLayout();
-}
-
-function bindRailSplitter() {
- let activePointer = null;
-
- elements.railSplitter.addEventListener("pointerdown", (event) => {
- if (state.railCollapsed || event.button !== 0) return;
- activePointer = event.pointerId;
- elements.railSplitter.setPointerCapture(event.pointerId);
- document.body.classList.add("is-resizing");
- });
-
- elements.railSplitter.addEventListener("pointermove", (event) => {
- if (activePointer === null || event.pointerId !== activePointer) return;
- const left = elements.controlRail.getBoundingClientRect().left;
- setRailWidth(event.clientX - left, { persist: false });
- });
-
- const endResize = (event) => {
- if (activePointer === null || (event && event.pointerId !== activePointer)) return;
- if (elements.railSplitter.hasPointerCapture(activePointer)) {
- elements.railSplitter.releasePointerCapture(activePointer);
- }
- activePointer = null;
- document.body.classList.remove("is-resizing");
- persistLayout();
- };
- elements.railSplitter.addEventListener("pointerup", endResize);
- elements.railSplitter.addEventListener("pointercancel", endResize);
-
- elements.railSplitter.addEventListener("dblclick", () => setRailWidth(RAIL_WIDTH_DEFAULT));
-
- elements.railSplitter.addEventListener("keydown", (event) => {
- const { min, max } = railWidthBounds();
- const current = Math.round(elements.controlRail.getBoundingClientRect().width) || state.railWidth;
- const next = {
- ArrowLeft: current - RAIL_KEYBOARD_STEP,
- ArrowRight: current + RAIL_KEYBOARD_STEP,
- Home: min,
- End: max,
- }[event.key];
- if (next === undefined) return;
- event.preventDefault();
- setRailWidth(next);
- });
}
async function loadFile(file) {
diff --git a/web-demo/public/index.html b/web-demo/public/index.html
index 8ec74ba..fe9c513 100644
--- a/web-demo/public/index.html
+++ b/web-demo/public/index.html
@@ -3,33 +3,8 @@
- MiniExcel Browser Lab
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ MiniExcel Browser Lab
@@ -50,23 +25,11 @@
.NET
Rust
-
-
-
-
-