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 - - - -
-