@keenaioc/data-view 0.2.0-alpha.7 → 0.2.0-alpha.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # Changelog
2
2
 
3
+ 歷史紀錄保留當時版本資訊;部分舊版已下架,歷史安裝/回退指令不代表仍可下載。alpha.8 的回退基準為 alpha.7。
4
+
5
+ ## 0.2.0-alpha.9 — 2026-09-06(Alpha)
6
+
7
+ - 修正:視圖設定的手機欄位、虛擬捲動、尚未排定入口與日期診斷勾選項統一為橫向靠左、垂直置中。勾選框固定 17px、不縮小;長標籤可換行。
8
+ - 修正:手機欄位每列保留固定 70px 排序區,包含兩個 32px 按鈕及 6px 間距;未選列保留相同空間,首末按鈕維持停用規則。排序按鈕補上可讀名稱與明確 button 型別。
9
+ - 範圍:僅共用設定面板標記與樣式,不修改資料載入、虛擬化或排程計算。
10
+ - 不相容變更/遷移:無;API、schemaVersion 與既有設定不變。
11
+ - 升級:`npm install --save-exact @keenaioc/data-view@0.2.0-alpha.9`,提交 dependency/lockfile,沿用 `@keenaioc/data-view/style.css`。不需要接入端 CSS 覆蓋。
12
+ - 回退:還原精確 alpha.8 dependency/lockfile 並重建;不需轉換資料或設定。
13
+ - 驗證:程式測試與建置結果記錄於來源發布紀錄;未將程式檢查當成瀏覽器尺寸或真機操作驗收。既有 Alpha 驗收限制仍適用。
14
+
15
+ ## 0.2.0-alpha.8 — 2026-09-06(公開 Alpha 測試版)
16
+
17
+ - 新增:CORE-01 `presentation` 自動/表格/卡片及選擇回呼;CORE-02 `chrome: "compact"` 精簡工具列、窄版圖示與更多操作;CORE-03 精簡表頭及 `headerStart`/`headerEnd` 插槽。
18
+ - 新增:CORE-05 `height` 控制整個 DataView 或 DataViewWorkspace;資料區扣除工具列與狀態列後使用剩餘高度,桌面、甘特、行事曆及卡片共用。既有 `viewportHeight` 保留資料区語意。
19
+ - 修正:CORE-04/T15 虛擬分組按鈕 31px → 正式 metrics 的 28px;保留外層 8px/內層 4px。精簡介面的工具列、視圖及排程導覽採用控制項尺寸;多選與複雜輸入維持詳情編輯。
20
+ - 修正:CORE-06 甘特桌面與手機議程同時顯示;寬窄切換優先於版面 display,日曆亦共用切換規則。
21
+ - 不相容變更:無必要 API 或資料遷移;未傳新 props 時保留標準表頭與資料區高度模式。自動呈現依元件寬度(767px)切換;窄桌面要維持表格可明確選擇 table。
22
+ - 需要調整:使用精簡版面的接入專案更新精確 dependency 與 CSS;不需要專案 CSS 覆蓋。不啟用依賴連動、資源負荷或自動改期。
23
+ - 升級:`npm install --save-exact @keenaioc/data-view@0.2.0-alpha.8`,同步更新套件 CSS 與 lockfile,先驗收一個試點。
24
+ - 設定/資料遷移:schemaVersion 維持 1,不修改資料。呈現偏好由 host 保存;既有受控 view 需自行選擇 workbench 密度。回退:還原 alpha.7 dependency/lockfile,移除新 props 後重建;保留 host 偏好備份。
25
+ - 限制:實際瀏覽器驗收受平台網址政策阻擋,尚未完成全寬/720px/手機與甘特切回表格的操作驗收;建置及程式測試不取代此驗收。
26
+
3
27
  ## 0.2.0-alpha.7 — 2026-09-06(Alpha)
4
28
 
5
29
  - 資料保護:加入公開資料隔離說明與打包前檢查;公開文件移除內部來源識別資訊。共用 API 不變;舊版公開內容不會因本次更新而自動消除。
package/INTEGRATION.md CHANGED
@@ -137,4 +137,47 @@ schemaVersion仍1,但分頁移除、人員/關聯條件與分組使用ID、
137
137
 
138
138
  舊人員分組收合鍵與名稱篩選需用旧資料及ID對照轉換;同名不明不可猜測。無法轉換的收合鍵首次展開,保留原備份以回退。
139
139
 
140
- 以 `npm install --save-exact @keenaioc/data-view@0.2.0-alpha.7` 接入單一測試專案,驗證 lockfile、型別、CSS 與實際操作,再决定正式 Site 的升級。回退至精確 alpha.5、其 lockfile 及設定快照並重建發布,不撤銷業務資料修改。發布套件不代表任何接入專案已完成操作驗收。
140
+ 以 `npm install --save-exact @keenaioc/data-view@0.2.0-alpha.7` 接入單一測試專案,驗證 lockfile、型別、CSS 與實際操作,再决定正式 Site 的升級。較舊版本已進入下架處理,不作為新接入的回退來源;alpha.8 的回退基準為 alpha.7,詳見下節。發布套件不代表任何接入專案已完成操作驗收。
141
+
142
+
143
+ ## alpha.8:CORE-01~06(Alpha)
144
+
145
+ 以下 API 自 alpha.8 提供。使用 `npm install --save-exact @keenaioc/data-view@0.2.0-alpha.8` 更新精確 dependency、lockfile 與套件 CSS;不要從其他專案複製內部 CSS。修正後瀏覽器驗收尚未完成,需先於測試專案核對下列行為。
146
+
147
+ ```tsx
148
+ import { DataView, type DataViewConfig } from "@keenaioc/data-view";
149
+ import "@keenaioc/data-view/style.css";
150
+
151
+ // host 提供資料、view、回呼,以及使用者+資料集/視圖範圍內的呈現偏好。
152
+ <DataView
153
+ fields={fields}
154
+ rows={rows}
155
+ getRowId={(row) => row.id}
156
+ view={view}
157
+ onViewChange={saveView}
158
+ presentation={presentation} // "auto" | "table" | "cards"
159
+ onPresentationChange={savePresentation}
160
+ chrome="compact"
161
+ height={availableHeight}
162
+ headerStart={<ScopeSelector />}
163
+ headerEnd={<ProjectSaveStatus />}
164
+ onCellCommit={saveCell}
165
+ />
166
+ ```
167
+
168
+ - `presentation` 是受控 prop,不會改資料或 viewType。auto 依元件寬度切換(767px 以下窄版);table 強制桌面表格/甘特/月曆,cards 強制卡片/議程。省略回呼便不顯示選擇器;host 必須保存及回傳選擇。它與 `view.mobile.presentation` 的 cards/compact 卡片密度是不同設定。
169
+ - `chrome="compact"` 精簡標題、視圖與工具列。搜尋/篩選/排序/分組保留;窄空間轉圖示,title 與 accessible name 保留完整文字與條件數。次要操作進「更多」,Tab 操作、Escape 關閉返回觸發鈕。標準模式仍是原工具列。
170
+ - `toolbarStart` 放常用業務範圍控制;`toolbarEnd` 在精簡模式收進更多,不能放必須持續可見的警告。持續狀態使用 `headerEnd`。內建載入完整性/查詢錯誤/寫入失敗/衝突提示不會收進更多。
171
+ - `headerStart`/`headerEnd` 是新增插槽,不取代必要筆數與儲存提示。可用 `features.viewTabs=false` 關掉內建種類切換、由 host 控制 viewType;DataViewWorkspace 的命名視圖仍維持同一份資料,精簡模式縮減其間距。
172
+ - `height` 是**整個元件**高度。DataViewWorkspace 還包含命名視圖與集合儲存狀態。可傳 px 數值或 CSS 長度;100% 必須有明確高度的父層,flex 父層需 `min-height:0`。推薦 host 量測網站剩餘空間後傳數值。極低高度若連工具列與警告也放不下,需由 host 提供更多空間。
173
+ - `viewportHeight` 仍是**資料區**高度,不含標題/工具列;未提供 height 時生效。新增 height 優先;避免將兩種數值誤當相同意義。手機卡片也遵守資料區限制。固定整體高度時由各視圖資料區負責垂直捲動;甘特左右捲動保持同步。
174
+ - 工作台密度須在受控 `view.density` 或新視圖 `initialView.density` 設為 workbench;既有設定不會被初始值覆蓋。表格資料列/表頭 32px、字 13px、膠囊 20px/11px;分組按鈕 28px、外層間距 8px/內層 4px。
175
+ - 複雜編輯器/多選/人員/關聯在詳情開啟,避免撐高或裁切行內操作。自訂 renderer/action 必須遵守格子空間,完整內容使用 quick view/detail。
176
+ - 舊設定無強制遷移;新偏好由 host 自己的設定服務保存,建議區分使用者+資料集+視圖。不要修改 backend query 或資料為了切换顯示模式。
177
+ - 回退:還原 alpha.7 的 dependency/lockfile,移除新 props(presentation/onPresentationChange/chrome/headerStart/headerEnd/height),保留偏好與 view 備份。無資料搬移。更舊版已涉及下架處理,不作為本版回退來源。
178
+ - 驗收必須涵蓋全寬、720px 分割窗、手機、表格→甘特→表格、分組、編輯、水平及連續捲動、儲存失敗/衝突、遠端尚未完整/完整/查詢失敗;只用合成資料。
179
+
180
+
181
+ ## alpha.9:設定面板勾選列修正
182
+
183
+ 精確升級至 `@keenaioc/data-view@0.2.0-alpha.9`,同步部署套件原始 CSS 與 lockfile。手機欄位清單共用橫向勾選樣式,長名稱換行,所有列保留固定排序區;虛擬捲動、排程診斷及甘特勾選项使用相同排列。不新增 props、不遷移 ViewConfig、不修改 host 的完整載入策略。回退精確 alpha.8 即可。
@@ -22,3 +22,18 @@
22
22
  - 甘特僅整日FS依賴,工作天只扣六日;需完整相關資料及原子scheduleGuard,遠端片段不能宣稱完整。沒有關鍵路徑、資源負荷、假日、公式或自動承諾排程。
23
23
 
24
24
  套件發布與安裝成功不代表業務接入或操作驗收完成。接入專案各自處理後端查詢、設定遷移、權限及保存;既有 alpha.5 試點不代表本版已驗收。
25
+
26
+
27
+ ## alpha.8 Alpha 限制
28
+
29
+ - CORE-01~06 與 T15 修正自 alpha.8 提供;alpha.7 不具備這些修正。本版為公開 Alpha,並非完成操作驗收的穩定版。
30
+ - 瀏覽器預覽曾成功顯示基底;本次修正後重載遭平台 URL 政策阻擋,桌機/720px/手機、更多選單鍵盤、實際高度與捲動仍待操作驗收。
31
+ - 精簡表頭在空間不足時允許標題/視圖區換行,工具列保持單行。任意 host 插槽內容需自行保持精簡;長警告不裁掉。
32
+ - 卡片/議程的觸控目標不強制等同桌機 28px 控制項。自訂欄位元件仍需依接入說明選擇詳情/快速檢視。
33
+ - height 百分比需要有高度的父層;此 API 不能修正 host 自己造成的雙重頁面捲動。DataViewWorkspace 的命名視圖列仍存在,需與單一 viewType 切換分別設定。
34
+ - 呈現偏好由 host 保存,不自動跨使用者/裝置同步;不啟用進度排程相關進階能力。
35
+
36
+
37
+ ## alpha.9 設定面板修正
38
+
39
+ 本次只修正勾選列排列與排序按鈕空間。未新增資料載入限制或改動自動載入;既有 Alpha 的實際瀏覽器及真機驗收限制持續有效。
package/README.md CHANGED
@@ -4,12 +4,14 @@ HomeYa 共用資料視圖引擎。以 既有工作台的既有表格成果為主
4
4
 
5
5
  套件不包含資料庫、業務流程、報價公式、使用者帳號或專案資料。接入專案透過 `DataView` 的 callbacks 處理讀取、寫入、附件、權限與稽核。
6
6
 
7
- 本文件版本:**`0.2.0-alpha.7`(Alpha)**。整合 T01~T19:工作台密度與 renderer、權限及狀態分離、資料列拖曳、並行寫入、手機獨立版面、排程空值、無分頁與遠端查詢契約。原先規劃的 alpha.6/alpha.7 合併為本版;alpha.6 未另行發布。
7
+ 本文件版本:**`0.2.0-alpha.9`(公開 Alpha 測試版)**。修正設定面板勾選列對齊、長名稱換行與手機欄位排序區固定空間,保留既有精簡介面與甘特響應修正。程式、型別、建置及打包檢查已通過;修正後瀏覽器操作驗收尚未完成,接入時須重測。
8
+
9
+ 既有基底:整合 T01~T19:工作台密度與 renderer、權限及狀態分離、資料列拖曳、並行寫入、手機獨立版面、排程空值、無分頁與遠端查詢契約。原先規劃的 alpha.6/alpha.7 合併為本版;alpha.6 未另行發布。
8
10
 
9
11
  本版保留 Alpha 限制:自動測試與建置通過,完整桌機/手機操作驗收尚未完成。先在測試專案接入,再決定是否升級正式系統。
10
12
 
11
13
  ```sh
12
- npm install --save-exact @keenaioc/data-view@0.2.0-alpha.7
14
+ npm install --save-exact @keenaioc/data-view@0.2.0-alpha.9
13
15
  ```
14
16
 
15
17
  必須固定精確版本;不要使用無版本安裝、`latest`、`next` 或範圍版本。
@@ -1 +1 @@
1
- {"version":3,"file":"data-view.d.ts","sourceRoot":"","sources":["../src/data-view.tsx"],"names":[],"mappings":"AAwCA,OAAO,KAAK,EAIV,aAAa,EAWd,MAAM,YAAY,CAAC;AAQpB,wBAAgB,QAAQ,CAAC,GAAG,EAAE,KAAK,EAAE,aAAa,CAAC,GAAG,CAAC,2CAEtD"}
1
+ {"version":3,"file":"data-view.d.ts","sourceRoot":"","sources":["../src/data-view.tsx"],"names":[],"mappings":"AA0CA,OAAO,KAAK,EAIV,aAAa,EAWd,MAAM,YAAY,CAAC;AAQpB,wBAAgB,QAAQ,CAAC,GAAG,EAAE,KAAK,EAAE,aAAa,CAAC,GAAG,CAAC,2CAEtD"}
package/dist/data-view.js CHANGED
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
- import { Fragment, useEffect, useMemo, useRef, useState, } from "react";
3
+ import { Fragment, Children, useEffect, useMemo, useRef, useState, } from "react";
4
+ import { DataViewToolbar } from "./toolbar.js";
4
5
  import { buildGroupTree, DataViewConflictError, filterRows, getFieldValue, getPinnedOffsets, getRightPinnedOffsets, MAX_GROUP_LEVELS, moveColumn, normalizeColumnWidth, normalizeViewConfig, retainViewReferences, searchRows, sortRows, validateValue, visibleFields, } from "./engine.js";
5
6
  import { FILTER_OPERATOR_LABELS, getFieldTypeDefinition, renderFieldValue, formatClipboardValue, formatReadableValue, formatReadableRow } from "./registry.js";
6
7
  import { canWriteField, createPasteChanges, encodeTSV, newOperationId, validateChanges, validateFieldChange } from "./operations.js";
@@ -35,6 +36,7 @@ function DataViewInner(props) {
35
36
  const rootRef = useRef(null);
36
37
  const scrollRef = useRef(null);
37
38
  const mobileRef = useRef(null);
39
+ useEffect(() => { setEditing(null); }, [props.presentation]);
38
40
  const [scrollTop, setScrollTop] = useState(0);
39
41
  const [focus, setFocus] = useState(null);
40
42
  const [anchor, setAnchor] = useState(null);
@@ -680,17 +682,15 @@ function DataViewInner(props) {
680
682
  cell?.scrollIntoView({ block: "nearest", inline: "nearest" });
681
683
  });
682
684
  }
683
- if (access === "denied")
684
- return _jsx(StateCard, { title: "\u7121\u6B0A\u9650", message: "\u4F60\u6C92\u6709\u67E5\u770B\u9019\u4EFD\u8CC7\u6599\u7684\u6B0A\u9650\u3002", tone: "warning" });
685
- if (loading)
686
- return _jsx(LoadingState, {});
687
- if (error)
688
- return _jsx(StateCard, { title: "\u8F09\u5165\u5931\u6557", message: error, tone: "error" });
685
+ if (access === "denied" || loading || error) {
686
+ const state = access === "denied" ? _jsx(StateCard, { title: "\u7121\u6B0A\u9650", message: "\u4F60\u6C92\u6709\u67E5\u770B\u9019\u4EFD\u8CC7\u6599\u7684\u6B0A\u9650\u3002", tone: "warning" }) : loading ? _jsx(LoadingState, {}) : _jsx(StateCard, { title: "\u8F09\u5165\u5931\u6557", message: error, tone: "error" });
687
+ return props.height === undefined ? state : _jsx("section", { className: "hy-dv", style: { height: props.height, overflow: "auto" }, "aria-label": title, children: state });
688
+ }
689
689
  const range = gridRange();
690
690
  const desktopRowIndices = new Map(desktopRows.map((row, index) => [getRowId(row), index]));
691
- return _jsxs("section", { ref: rootRef, className: `hy-dv density-${activeView.density ?? "standard"}`, style: { "--hy-row-height": `${rowHeight}px`, "--hy-header-height": `${metrics.headerHeight}px`, "--hy-group-height": `${metrics.groupHeight}px`, "--hy-cell-font": `${metrics.cellFontSize}px`, "--hy-cell-line": `${metrics.cellLineHeight}px`, "--hy-header-font": `${metrics.headerFontSize}px`, "--hy-cell-padding": `${metrics.cellPadding}px`, "--hy-header-left": `${metrics.headerPaddingLeft}px`, "--hy-header-right": `${metrics.headerPaddingRight}px`, "--hy-control-height": `${metrics.controlHeight}px`, "--hy-action-height": `${metrics.actionHeight}px`, "--hy-badge-height": `${metrics.badgeHeight}px`, "--hy-badge-font": `${metrics.badgeFontSize}px`, "--hy-badge-padding": `${metrics.badgePadding}px`, "--hy-viewport-height": typeof props.viewportHeight === "number" ? `${props.viewportHeight}px` : props.viewportHeight ?? "560px", "--hy-scrollbar-height": `${metrics.scrollbarHeight}px`, "--hy-scrollbar-thumb": `${metrics.scrollbarThumb}px` }, "aria-label": title, onKeyDown: handleKey, onCopy: handleCopy, onPaste: handlePaste, children: [_jsxs("header", { className: "hy-dv-titlebar", children: [_jsxs("div", { children: [_jsx("h2", { children: title }), _jsx("span", { children: props.remote ? `符合 ${remoteStale ? "…" : props.remote.total ?? "…"} 筆,已載入 ${processedRows.length} 筆${props.remote.complete && !remoteStale ? "(完整)" : "(尚未完整)"}` : `${processedRows.length} / ${rows.length} 筆` })] }), _jsxs("div", { className: "hy-dv-title-status", children: [selected.length > 0 && _jsxs("span", { children: ["\u5DF2\u9078 ", selected.length, " \u7B46"] }), !props.hideViewSaveStatus && _jsx(SaveBadge, { state: viewSaveState, prefix: "\u6AA2\u8996" }), !props.hideViewSaveStatus && (viewSaveState === "failed" || viewSaveState === "conflict") && _jsx("button", { onClick: () => setView(viewRef.current), children: "\u91CD\u8A66\u5132\u5B58" })] })] }), features.viewTabs !== false && _jsxs("nav", { className: "hy-dv-view-tabs", "aria-label": "\u8CC7\u6599\u8996\u5716", children: [[["table", "資料表格"], ["gantt", "甘特圖"], ["calendar", "行事曆"]].map(([type, label]) => _jsx("button", { "aria-pressed": activeView.viewType === type, disabled: permissions?.canSwitchView === false, onClick: () => { patchView({ viewType: type }, configurable ? "layout" : "navigation"); setEditing(null); setPanel(null); }, children: label }, type)), configurable && features.viewSettings !== false && _jsx("button", { onClick: () => setPanel(panel === "settings" ? null : "settings"), children: activeView.viewType === "gantt" ? "甘特圖設定" : activeView.viewType === "calendar" ? "行事曆設定" : "視圖設定" })] }), _jsxs("div", { className: "hy-dv-toolbar", children: [props.toolbarStart, features.search !== false && permissions?.canSearch !== false && _jsxs("label", { className: "hy-dv-search", children: [_jsx("span", { "aria-hidden": true, children: "\u2315" }), _jsx("input", { value: search, onChange: (event) => patchLocation({ search: event.target.value, tableTop: 0, ganttTop: 0 }), placeholder: searchPlaceholder })] }), canFilter && features.filter !== false && _jsxs(ToolbarButton, { active: panel === "filter", onClick: () => setPanel(panel === "filter" ? null : "filter"), children: ["\u7BE9\u9078 ", activeView.filters.rules.length || ""] }), canSort && features.sort !== false && _jsxs(ToolbarButton, { active: panel === "sort", onClick: () => setPanel(panel === "sort" ? null : "sort"), children: ["\u6392\u5E8F ", activeView.sorts.length || ""] }), configurable && features.group !== false && _jsxs(ToolbarButton, { active: panel === "group", onClick: () => setPanel(panel === "group" ? null : "group"), children: ["\u5206\u7D44 ", activeView.groups.length || ""] }), configurable && features.columns !== false && _jsx(ToolbarButton, { active: panel === "columns", onClick: () => setPanel(panel === "columns" ? null : "columns"), children: "\u6B04\u4F4D\u8A2D\u5B9A" }), configurable && features.colors !== false && _jsx(ToolbarButton, { active: panel === "colors", onClick: () => setPanel(panel === "colors" ? null : "colors"), children: "\u586B\u8272" }), canManageFields && _jsxs(_Fragment, { children: [_jsx(ToolbarButton, { onClick: () => { setPanel(null); setDialog("add-field"); }, children: "\uFF0B \u65B0\u589E\u6B04\u4F4D" }), _jsx(ToolbarButton, { onClick: () => { setPanel(null); setDialog("fields"); }, children: "\u7BA1\u7406\u6B04\u4F4D" })] }), onBatchCommit && _jsxs(_Fragment, { children: [features.batch !== false && _jsxs("button", { disabled: !selectedRows.length || busy, onClick: () => { setPanel(null); setDialog("batch"); }, children: ["\u6279\u6B21\u4FEE\u6539 ", selectedRows.length || ""] }), features.undo !== false && _jsx("button", { disabled: !undoStack.length || busy, onClick: () => void historyAction("undo"), children: "\u5FA9\u539F" }), features.redo !== false && _jsx("button", { disabled: !redoStack.length || busy, onClick: () => void historyAction("redo"), children: "\u91CD\u505A" })] }), selected.length > 0 && _jsx(ToolbarButton, { onClick: () => changeSelection([]), children: "\u6E05\u9664\u9078\u53D6" }), activeView.groups.length > 0 && permissions?.canCollapseGroups !== false && _jsxs(_Fragment, { children: [_jsx(ToolbarButton, { onClick: () => setAllGroups(true), children: "\u5168\u90E8\u6536\u5408" }), _jsx(ToolbarButton, { onClick: () => setAllGroups(false), children: "\u5168\u90E8\u5C55\u958B" })] }), props.toolbarEnd] }), move.message && _jsx("p", { role: "status", children: move.message }), operation.state !== "idle" && _jsxs("div", { className: `hy-dv-operation is-${operation.state}`, role: operation.state === "failed" || operation.state === "conflict" ? "alert" : "status", children: [_jsx(SaveBadge, { state: operation.state }), _jsx("span", { children: operation.message })] }), panel && (configurable || panel === "filter" && canFilter || panel === "sort" && canSort) && _jsxs("div", { className: "hy-dv-panel-wrap", children: [panel === "filter" && _jsx(FilterPanel, { fields: availableFields, view: activeView, onChange: (v) => setView(v, "query"), onClose: () => setPanel(null) }), panel === "sort" && _jsx(SortPanel, { fields: availableFields, view: activeView, onChange: (v) => setView(v, "query"), onClose: () => setPanel(null) }), panel === "group" && _jsx(GroupPanel, { fields: fields, view: activeView, onChange: setView, onClose: () => setPanel(null) }), panel === "columns" && _jsx(ColumnPanel, { fields: availableFields, view: { ...activeView, columns: activeView.columns.filter((c) => availableFields.some((f) => f.id === c.fieldId)) }, onChange: setView, onClose: () => setPanel(null), dragged: draggedColumn, setDragged: setDraggedColumn, drop: dropColumn }), panel === "settings" && _jsx(PanelShell, { title: "\u8996\u5716\u8A2D\u5B9A", onClose: () => setPanel(null), children: _jsx(ViewSettings, { fields: availableFields, view: activeView, onChange: setView }) }), panel === "colors" && _jsx(PanelShell, { title: "\u586B\u8272\u898F\u5247", onClose: () => setPanel(null), children: _jsx(ColorSettings, { fields: availableFields, view: activeView, onChange: setView }) })] }), remoteStale || props.remote?.loading && !processedRows.length ? _jsx(LoadingState, {}) : processedRows.length === 0 && props.remote?.error ? _jsx(StateCard, { title: "\u67E5\u8A62\u5931\u6557", message: props.remote.error, tone: "error" }) : processedRows.length === 0 ? _jsx(StateCard, { title: "\u6C92\u6709\u7B26\u5408\u689D\u4EF6\u7684\u8CC7\u6599", message: rows.length === 0 ? emptyMessage : "請調整搜尋或篩選條件。" }) : activeView.viewType !== "table" ? _jsx(ScheduleView, { metrics: metrics, getRowLabel: readableLabel, groupLabel: groupLabel, onLoadNearEnd: loadNearEnd, allRows: rows, dependencies: ganttDependencies, onDependencyChange: onGanttDependencyChange && permissions?.canManageDependencies === true ? changeDependency : undefined, onScheduleCommit: onBatchCommit ? execute : undefined, rows: processedRows, renderMobileRow: (row) => _jsx("dl", { className: "hy-dv-schedule-fields", children: mobileFields.filter((field) => canRead(row, field)).map((field) => _jsxs(Fragment, { children: [_jsx("dt", { children: field.label }), _jsx("dd", { children: renderCell(row, field, "mobile") })] }, field.id)) }), onLocationChange: patchLocation, onToggleGroup: toggleGroup, renderLeftHeader: () => _jsxs("tr", { children: [_jsx("th", { className: "hy-dv-open-header is-pinned", style: { left: 0 }, children: "\u8A73\u60C5" }), visible.map((v) => renderColumnHeader(v.field, v.column, true))] }), renderLeftRow: (row) => _jsxs("tr", { children: [_jsx("td", { className: "is-pinned", style: { left: 0 }, children: _jsx("button", { "aria-label": `開啟 ${getRowId(row)} 詳情`, onClick: () => openRow(row), children: "\u2197" }) }), visible.map(({ field }) => _jsx("td", { className: ganttLeftOffsets.has(field.id) || ganttRightOffsets.has(field.id) ? "is-pinned" : "", style: { left: ganttLeftOffsets.get(field.id), right: ganttRightOffsets.get(field.id), backgroundColor: colorMap.get(getRowId(row))?.get(field.id) ?? colorMap.get(getRowId(row))?.get("row") }, children: _jsx("div", { className: "hy-dv-gantt-cell", children: renderCell(row, field, "gantt") }) }, field.id))] }, getRowId(row)), fields: fields, getRowId: getRowId, view: activeView, permissions: permissions, busy: busy, onCommit: onBatchCommit ? (changes) => execute({ id: newOperationId(), reason: "edit", changes }) : undefined, onOpen: openRow }, activeView.viewType) : _jsxs(_Fragment, { children: [_jsx("div", { className: "hy-dv-desktop", children: _jsx("div", { ref: scrollRef, className: `hy-dv-table-scroll ${virtual ? "is-virtual" : ""}`, onScroll: (event) => { setScrollTop(event.currentTarget.scrollTop); loadNearEnd(event.currentTarget); if (bottomRef.current)
692
- bottomRef.current.scrollLeft = event.currentTarget.scrollLeft; patchLocation({ tableTop: event.currentTarget.scrollTop, tableLeft: event.currentTarget.scrollLeft }); }, children: _jsxs("table", { className: "hy-dv-table", style: { width: tableWidth }, "aria-label": "\u8CC7\u6599\u5132\u5B58\u683C", "aria-rowcount": desktopRows.length + 1, children: [_jsxs("colgroup", { children: [controlColumn && _jsx("col", { style: { width: controlWidth } }), visible.map(({ field, column }) => _jsx("col", { style: { width: column.width } }, field.id)), canManageFields && _jsx("col", { style: { width: addColumnWidth } })] }), _jsx("thead", { children: _jsxs("tr", { children: [controlColumn && _jsx("th", { className: "hy-dv-selection is-pinned", style: { left: 0 }, children: selectable && _jsx("input", { type: "checkbox", checked: allPageSelected, onChange: togglePageSelection, "aria-label": props.remote && !props.remote.complete ? "選取目前已載入資料" : "選取全部符合條件資料" }) }), visible.map(({ field, column }) => renderColumnHeader(field, column)), canManageFields && _jsx("th", { className: "hy-dv-add-column is-pinned", style: { right: 0 }, children: _jsx("button", { type: "button", disabled: busy, "aria-label": "\u65B0\u589E\u6B04\u4F4D", title: "\u65B0\u589E\u6B04\u4F4D", onClick: () => { setPanel(null); setDialog("add-field"); }, children: "\uFF0B" }) })] }) }), _jsxs("tbody", { children: [virtual && virtualStart > 0 && _jsx("tr", { "aria-hidden": true, children: _jsx("td", { colSpan: tableColumnCount, style: { height: windowed.before, padding: 0, border: 0 } }) }), flatEntries.slice(virtualStart, virtualEnd).map((entry) => entry.kind === "row" ? renderDesktopRow(entry.row) : _jsx("tr", { "data-group-key": entry.node.key, className: `hy-dv-group depth-${entry.node.depth} ${move.target?.position === "groupEnd" && move.target.targetGroupPath.at(-1)?.key === entry.node.key ? "is-drop-after" : ""}`, style: { height: entryHeights[flatEntries.indexOf(entry)] }, children: _jsx("td", { colSpan: tableColumnCount, children: _jsxs("button", { type: "button", onClick: () => toggleGroup(entry.node.key), "aria-expanded": !collapsed.has(entry.node.key), children: [collapsed.has(entry.node.key) ? "›" : "⌄", groupLabel(entry.node)] }) }) }, `group:${entry.node.key}`)), virtual && virtualEnd < flatEntries.length && _jsx("tr", { "aria-hidden": true, children: _jsx("td", { colSpan: tableColumnCount, style: { height: windowed.after, padding: 0, border: 0 } }) })] })] }) }) }), _jsx(VirtualList, { items: flatEntries, itemKey: (e) => e.kind === "group" ? `g:${e.node.key}` : getRowId(e.row), estimate: (e) => e.kind === "group" ? 40 : activeView.mobile?.presentation === "compact" ? 80 + mobileFields.length * 28 : 100 + mobileFields.length * 36, className: `hy-dv-mobile mobile-${activeView.mobile?.presentation ?? "cards"}`, initialTop: activeView.location?.mobileTop, onScroll: (el) => { loadNearEnd(el); patchLocation({ mobileTop: el.scrollTop }); }, renderItem: (entry) => entry.kind === "group" ? _jsxs("button", { "data-group-key": entry.node.key, className: "hy-dv-mobile-group-title", "aria-expanded": !collapsed.has(entry.node.key), onClick: () => toggleGroup(entry.node.key), children: [collapsed.has(entry.node.key) ? "›" : "⌄", groupLabel(entry.node)] }) : renderMobileCard(entry.row) }), _jsxs("p", { className: "hy-dv-keyboard-help", children: ["\u65B9\u5411\u9375\u79FB\u52D5 \u00B7 Shift\uFF0B\u65B9\u5411\u9375\u9078\u7BC4\u570D \u00B7 Enter \u7DE8\u8F2F \u00B7 Ctrl/Cmd\uFF0BC\uFF0FV \u8907\u88FD\u8CBC\u4E0A \u00B7 Delete \u6E05\u7A7A\u9810\u89BD", virtual ? ` · 連續捲動,共 ${desktopRows.length} 筆` : ""] }), props.bottomScrollbar !== false && tableWidth > viewportWidth && _jsx("div", { ref: bottomRef, className: "hy-dv-bottom-scroll", "aria-label": "\u8868\u683C\u6C34\u5E73\u6372\u52D5", onScroll: (e) => { if (scrollRef.current)
693
- scrollRef.current.scrollLeft = e.currentTarget.scrollLeft; }, children: _jsx("div", { style: { width: tableWidth, height: 1 } }) })] }), props.remote && _jsxs("div", { className: "hy-dv-load-status", role: props.remote.error ? "alert" : "status", children: [props.remote.error || (props.remote.loading || remoteStale ? "正在載入符合條件的資料…" : props.remote.complete ? `已完整載入 ${processedRows.length} 筆` : `尚未完整:已載入 ${processedRows.length}/${props.remote.total ?? "…"} 筆`), !props.remote.complete && !props.remote.loading && !remoteStale && _jsx("button", { onClick: props.remote.error ? props.remote.onRetry : props.remote.onLoadMore, children: props.remote.error ? "重新查詢" : "繼續載入" })] }), quick && (() => { const row = rows.find((r) => getRowId(r) === quick.rowId); const field = fields.find((f) => f.id === quick.fieldId); return row && field && !field.internal && canRead(row, field) && _jsxs(DataViewDialog, { title: field.label, onClose: () => setQuick(null), children: [field.renderQuickView?.({ row, field, value: getFieldValue(row, field), close: () => setQuick(null), openRow: () => { setQuick(null); openRow(row); } }), canEdit(row, field) && _jsx("button", { onClick: () => { setQuick(null); setDetailId(getRowId(row)); setEditing({ rowId: getRowId(row), fieldId: field.id, surface: "detail" }); }, children: "\u7DE8\u8F2F" })] }); })(), detailRow && _jsxs(DataViewDialog, { title: readableLabel(detailRow), busy: busy, onClose: () => { setDetailId(null); setEditing(null); }, children: [props.renderDetail ? props.renderDetail(detailContext(detailRow)) : _jsx("div", { className: "hy-dv-detail", children: availableFields.filter((field) => canRead(detailRow, field)).map((field) => _jsxs("label", { children: [_jsx("span", { children: field.label }), renderCell(detailRow, field, "detail")] }, field.id)) }), features.copy !== false && _jsx("button", { onClick: async () => { try {
691
+ return _jsxs("section", { ref: rootRef, className: `hy-dv density-${activeView.density ?? "standard"} presentation-${props.presentation ?? "auto"} chrome-${props.chrome ?? "standard"} ${props.height !== undefined ? "has-total-height" : ""}`, style: { height: props.height, "--hy-row-height": `${rowHeight}px`, "--hy-header-height": `${metrics.headerHeight}px`, "--hy-group-height": `${metrics.groupHeight}px`, "--hy-cell-font": `${metrics.cellFontSize}px`, "--hy-cell-line": `${metrics.cellLineHeight}px`, "--hy-header-font": `${metrics.headerFontSize}px`, "--hy-cell-padding": `${metrics.cellPadding}px`, "--hy-header-left": `${metrics.headerPaddingLeft}px`, "--hy-header-right": `${metrics.headerPaddingRight}px`, "--hy-control-height": `${metrics.controlHeight}px`, "--hy-action-height": `${metrics.actionHeight}px`, "--hy-badge-height": `${metrics.badgeHeight}px`, "--hy-badge-font": `${metrics.badgeFontSize}px`, "--hy-badge-padding": `${metrics.badgePadding}px`, "--hy-viewport-height": typeof props.viewportHeight === "number" ? `${props.viewportHeight}px` : props.viewportHeight ?? "560px", "--hy-scrollbar-height": `${metrics.scrollbarHeight}px`, "--hy-scrollbar-thumb": `${metrics.scrollbarThumb}px` }, "aria-label": title, onKeyDown: handleKey, onCopy: handleCopy, onPaste: handlePaste, children: [_jsxs("div", { className: "hy-dv-heading", children: [_jsxs("header", { className: "hy-dv-titlebar", children: [props.headerStart, _jsxs("div", { children: [_jsx("h2", { children: title }), _jsx("span", { children: props.remote ? `符合 ${remoteStale ? "…" : props.remote.total ?? "…"} 筆,已載入 ${processedRows.length} 筆${props.remote.complete && !remoteStale ? "(完整)" : "(尚未完整)"}` : `${processedRows.length} / ${rows.length} 筆` })] }), _jsxs("div", { className: "hy-dv-title-status", children: [selected.length > 0 && _jsxs("span", { children: ["\u5DF2\u9078 ", selected.length, " \u7B46"] }), !props.hideViewSaveStatus && _jsx(SaveBadge, { state: viewSaveState, prefix: "\u6AA2\u8996" }), !props.hideViewSaveStatus && (viewSaveState === "failed" || viewSaveState === "conflict") && _jsx("button", { onClick: () => setView(viewRef.current), children: "\u91CD\u8A66\u5132\u5B58" })] }), props.headerEnd] }), features.viewTabs !== false && _jsxs("nav", { className: "hy-dv-view-tabs", "aria-label": "\u8CC7\u6599\u8996\u5716", children: [[["table", "資料表格"], ["gantt", "甘特圖"], ["calendar", "行事曆"]].map(([type, label]) => _jsx("button", { "aria-pressed": activeView.viewType === type, disabled: permissions?.canSwitchView === false, onClick: () => { patchView({ viewType: type }, configurable ? "layout" : "navigation"); setEditing(null); setPanel(null); }, children: label }, type)), configurable && features.viewSettings !== false && _jsx("button", { onClick: () => setPanel(panel === "settings" ? null : "settings"), children: activeView.viewType === "gantt" ? "甘特圖設定" : activeView.viewType === "calendar" ? "行事曆設定" : "視圖設定" })] })] }), _jsxs(DataViewToolbar, { compact: props.chrome === "compact", start: props.toolbarStart, end: props.toolbarEnd, secondary: _jsxs(_Fragment, { children: [configurable && features.columns !== false && _jsx(ToolbarButton, { active: panel === "columns", onClick: () => setPanel(panel === "columns" ? null : "columns"), children: "\u6B04\u4F4D\u8A2D\u5B9A" }), configurable && features.colors !== false && _jsx(ToolbarButton, { active: panel === "colors", onClick: () => setPanel(panel === "colors" ? null : "colors"), children: "\u586B\u8272" }), canManageFields && _jsxs(_Fragment, { children: [_jsx(ToolbarButton, { onClick: () => { setPanel(null); setDialog("add-field"); }, children: "\uFF0B \u65B0\u589E\u6B04\u4F4D" }), _jsx(ToolbarButton, { onClick: () => { setPanel(null); setDialog("fields"); }, children: "\u7BA1\u7406\u6B04\u4F4D" })] }), onBatchCommit && _jsxs(_Fragment, { children: [features.batch !== false && _jsxs("button", { disabled: !selectedRows.length || busy, onClick: () => { setPanel(null); setDialog("batch"); }, children: ["\u6279\u6B21\u4FEE\u6539 ", selectedRows.length || ""] }), features.undo !== false && _jsx("button", { disabled: !undoStack.length || busy, onClick: () => void historyAction("undo"), children: "\u5FA9\u539F" }), features.redo !== false && _jsx("button", { disabled: !redoStack.length || busy, onClick: () => void historyAction("redo"), children: "\u91CD\u505A" })] }), selected.length > 0 && _jsx(ToolbarButton, { onClick: () => changeSelection([]), children: "\u6E05\u9664\u9078\u53D6" }), activeView.groups.length > 0 && permissions?.canCollapseGroups !== false && _jsxs(_Fragment, { children: [_jsx(ToolbarButton, { onClick: () => setAllGroups(true), children: "\u5168\u90E8\u6536\u5408" }), _jsx(ToolbarButton, { onClick: () => setAllGroups(false), children: "\u5168\u90E8\u5C55\u958B" })] })] }), children: [features.search !== false && permissions?.canSearch !== false && _jsxs("label", { className: "hy-dv-search", children: [_jsx("span", { "aria-hidden": true, children: "\u2315" }), _jsx("input", { "aria-label": searchPlaceholder, value: search, onChange: (event) => patchLocation({ search: event.target.value, tableTop: 0, ganttTop: 0 }), placeholder: searchPlaceholder })] }), canFilter && features.filter !== false && _jsxs(ToolbarButton, { active: panel === "filter" || activeView.filters.rules.length > 0, onClick: () => setPanel(panel === "filter" ? null : "filter"), children: ["\u7BE9\u9078 ", activeView.filters.rules.length || ""] }), canSort && features.sort !== false && _jsxs(ToolbarButton, { active: panel === "sort" || activeView.sorts.length > 0, onClick: () => setPanel(panel === "sort" ? null : "sort"), children: ["\u6392\u5E8F ", activeView.sorts.length || ""] }), configurable && features.group !== false && _jsxs(ToolbarButton, { active: panel === "group" || activeView.groups.length > 0, onClick: () => setPanel(panel === "group" ? null : "group"), children: ["\u5206\u7D44 ", activeView.groups.length || ""] }), props.onPresentationChange && _jsxs("select", { "aria-label": "\u5448\u73FE\u65B9\u5F0F", value: props.presentation ?? "auto", onChange: (e) => props.onPresentationChange?.(e.target.value), children: [_jsx("option", { value: "auto", children: "\u81EA\u52D5" }), _jsx("option", { value: "table", children: "\u8868\u683C" }), _jsx("option", { value: "cards", children: "\u5361\u7247" })] })] }), move.message && _jsx("p", { role: "status", children: move.message }), operation.state !== "idle" && _jsxs("div", { className: `hy-dv-operation is-${operation.state}`, role: operation.state === "failed" || operation.state === "conflict" ? "alert" : "status", children: [_jsx(SaveBadge, { state: operation.state }), _jsx("span", { children: operation.message })] }), panel && (configurable || panel === "filter" && canFilter || panel === "sort" && canSort) && _jsxs("div", { className: "hy-dv-panel-wrap", children: [panel === "filter" && _jsx(FilterPanel, { fields: availableFields, view: activeView, onChange: (v) => setView(v, "query"), onClose: () => setPanel(null) }), panel === "sort" && _jsx(SortPanel, { fields: availableFields, view: activeView, onChange: (v) => setView(v, "query"), onClose: () => setPanel(null) }), panel === "group" && _jsx(GroupPanel, { fields: fields, view: activeView, onChange: setView, onClose: () => setPanel(null) }), panel === "columns" && _jsx(ColumnPanel, { fields: availableFields, view: { ...activeView, columns: activeView.columns.filter((c) => availableFields.some((f) => f.id === c.fieldId)) }, onChange: setView, onClose: () => setPanel(null), dragged: draggedColumn, setDragged: setDraggedColumn, drop: dropColumn }), panel === "settings" && _jsx(PanelShell, { title: "\u8996\u5716\u8A2D\u5B9A", onClose: () => setPanel(null), children: _jsx(ViewSettings, { fields: availableFields, view: activeView, onChange: setView }) }), panel === "colors" && _jsx(PanelShell, { title: "\u586B\u8272\u898F\u5247", onClose: () => setPanel(null), children: _jsx(ColorSettings, { fields: availableFields, view: activeView, onChange: setView }) })] }), _jsx("div", { className: "hy-dv-body", children: remoteStale || props.remote?.loading && !processedRows.length ? _jsx(LoadingState, {}) : processedRows.length === 0 && props.remote?.error ? _jsx(StateCard, { title: "\u67E5\u8A62\u5931\u6557", message: props.remote.error, tone: "error" }) : processedRows.length === 0 ? _jsx(StateCard, { title: "\u6C92\u6709\u7B26\u5408\u689D\u4EF6\u7684\u8CC7\u6599", message: rows.length === 0 ? emptyMessage : "請調整搜尋或篩選條件。" }) : activeView.viewType !== "table" ? _jsx(ScheduleView, { metrics: metrics, getRowLabel: readableLabel, groupLabel: groupLabel, onLoadNearEnd: loadNearEnd, allRows: rows, dependencies: ganttDependencies, onDependencyChange: onGanttDependencyChange && permissions?.canManageDependencies === true ? changeDependency : undefined, onScheduleCommit: onBatchCommit ? execute : undefined, rows: processedRows, renderMobileRow: (row) => _jsx("dl", { className: "hy-dv-schedule-fields", children: mobileFields.filter((field) => canRead(row, field)).map((field) => _jsxs(Fragment, { children: [_jsx("dt", { children: field.label }), _jsx("dd", { children: renderCell(row, field, "mobile") })] }, field.id)) }), onLocationChange: patchLocation, onToggleGroup: toggleGroup, renderLeftHeader: () => _jsxs("tr", { children: [_jsx("th", { className: "hy-dv-open-header is-pinned", style: { left: 0 }, children: "\u8A73\u60C5" }), visible.map((v) => renderColumnHeader(v.field, v.column, true))] }), renderLeftRow: (row) => _jsxs("tr", { children: [_jsx("td", { className: "is-pinned", style: { left: 0 }, children: _jsx("button", { "aria-label": `開啟 ${getRowId(row)} 詳情`, onClick: () => openRow(row), children: "\u2197" }) }), visible.map(({ field }) => _jsx("td", { className: ganttLeftOffsets.has(field.id) || ganttRightOffsets.has(field.id) ? "is-pinned" : "", style: { left: ganttLeftOffsets.get(field.id), right: ganttRightOffsets.get(field.id), backgroundColor: colorMap.get(getRowId(row))?.get(field.id) ?? colorMap.get(getRowId(row))?.get("row") }, children: _jsx("div", { className: "hy-dv-gantt-cell", children: renderCell(row, field, "gantt") }) }, field.id))] }, getRowId(row)), fields: fields, getRowId: getRowId, view: activeView, permissions: permissions, busy: busy, onCommit: onBatchCommit ? (changes) => execute({ id: newOperationId(), reason: "edit", changes }) : undefined, onOpen: openRow }, activeView.viewType) : _jsxs(_Fragment, { children: [_jsx("div", { className: "hy-dv-desktop", children: _jsx("div", { ref: scrollRef, className: `hy-dv-table-scroll ${virtual ? "is-virtual" : ""}`, onScroll: (event) => { setScrollTop(event.currentTarget.scrollTop); loadNearEnd(event.currentTarget); if (bottomRef.current)
692
+ bottomRef.current.scrollLeft = event.currentTarget.scrollLeft; patchLocation({ tableTop: event.currentTarget.scrollTop, tableLeft: event.currentTarget.scrollLeft }); }, children: _jsxs("table", { className: "hy-dv-table", style: { width: tableWidth }, "aria-label": "\u8CC7\u6599\u5132\u5B58\u683C", "aria-rowcount": desktopRows.length + 1, children: [_jsxs("colgroup", { children: [controlColumn && _jsx("col", { style: { width: controlWidth } }), visible.map(({ field, column }) => _jsx("col", { style: { width: column.width } }, field.id)), canManageFields && _jsx("col", { style: { width: addColumnWidth } })] }), _jsx("thead", { children: _jsxs("tr", { children: [controlColumn && _jsx("th", { className: "hy-dv-selection is-pinned", style: { left: 0 }, children: selectable && _jsx("input", { type: "checkbox", checked: allPageSelected, onChange: togglePageSelection, "aria-label": props.remote && !props.remote.complete ? "選取目前已載入資料" : "選取全部符合條件資料" }) }), visible.map(({ field, column }) => renderColumnHeader(field, column)), canManageFields && _jsx("th", { className: "hy-dv-add-column is-pinned", style: { right: 0 }, children: _jsx("button", { type: "button", disabled: busy, "aria-label": "\u65B0\u589E\u6B04\u4F4D", title: "\u65B0\u589E\u6B04\u4F4D", onClick: () => { setPanel(null); setDialog("add-field"); }, children: "\uFF0B" }) })] }) }), _jsxs("tbody", { children: [virtual && virtualStart > 0 && _jsx("tr", { "aria-hidden": true, children: _jsx("td", { colSpan: tableColumnCount, style: { height: windowed.before, padding: 0, border: 0 } }) }), flatEntries.slice(virtualStart, virtualEnd).map((entry) => entry.kind === "row" ? renderDesktopRow(entry.row) : _jsx("tr", { "data-group-key": entry.node.key, className: `hy-dv-group depth-${entry.node.depth} ${move.target?.position === "groupEnd" && move.target.targetGroupPath.at(-1)?.key === entry.node.key ? "is-drop-after" : ""}`, style: { height: entryHeights[flatEntries.indexOf(entry)] }, children: _jsx("td", { colSpan: tableColumnCount, children: _jsxs("button", { type: "button", onClick: () => toggleGroup(entry.node.key), "aria-expanded": !collapsed.has(entry.node.key), children: [collapsed.has(entry.node.key) ? "›" : "⌄", groupLabel(entry.node)] }) }) }, `group:${entry.node.key}`)), virtual && virtualEnd < flatEntries.length && _jsx("tr", { "aria-hidden": true, children: _jsx("td", { colSpan: tableColumnCount, style: { height: windowed.after, padding: 0, border: 0 } }) })] })] }) }) }), _jsx(VirtualList, { items: flatEntries, itemKey: (e) => e.kind === "group" ? `g:${e.node.key}` : getRowId(e.row), estimate: (e) => e.kind === "group" ? 40 : activeView.mobile?.presentation === "compact" ? 80 + mobileFields.length * 28 : 100 + mobileFields.length * 36, className: `hy-dv-mobile mobile-${activeView.mobile?.presentation ?? "cards"}`, initialTop: activeView.location?.mobileTop, onScroll: (el) => { loadNearEnd(el); patchLocation({ mobileTop: el.scrollTop }); }, renderItem: (entry) => entry.kind === "group" ? _jsxs("button", { "data-group-key": entry.node.key, className: "hy-dv-mobile-group-title", "aria-expanded": !collapsed.has(entry.node.key), onClick: () => toggleGroup(entry.node.key), children: [collapsed.has(entry.node.key) ? "›" : "⌄", groupLabel(entry.node)] }) : renderMobileCard(entry.row) }), _jsxs("p", { className: "hy-dv-keyboard-help", children: ["\u65B9\u5411\u9375\u79FB\u52D5 \u00B7 Shift\uFF0B\u65B9\u5411\u9375\u9078\u7BC4\u570D \u00B7 Enter \u7DE8\u8F2F \u00B7 Ctrl/Cmd\uFF0BC\uFF0FV \u8907\u88FD\u8CBC\u4E0A \u00B7 Delete \u6E05\u7A7A\u9810\u89BD", virtual ? ` · 連續捲動,共 ${desktopRows.length} 筆` : ""] }), props.bottomScrollbar !== false && tableWidth > viewportWidth && _jsx("div", { ref: bottomRef, className: "hy-dv-bottom-scroll", "aria-label": "\u8868\u683C\u6C34\u5E73\u6372\u52D5", onScroll: (e) => { if (scrollRef.current)
693
+ scrollRef.current.scrollLeft = e.currentTarget.scrollLeft; }, children: _jsx("div", { style: { width: tableWidth, height: 1 } }) })] }) }), props.remote && _jsxs("div", { className: "hy-dv-load-status", role: props.remote.error ? "alert" : "status", children: [props.remote.error || (props.remote.loading || remoteStale ? "正在載入符合條件的資料…" : props.remote.complete ? `已完整載入 ${processedRows.length} 筆` : `尚未完整:已載入 ${processedRows.length}/${props.remote.total ?? "…"} 筆`), !props.remote.complete && !props.remote.loading && !remoteStale && _jsx("button", { onClick: props.remote.error ? props.remote.onRetry : props.remote.onLoadMore, children: props.remote.error ? "重新查詢" : "繼續載入" })] }), quick && (() => { const row = rows.find((r) => getRowId(r) === quick.rowId); const field = fields.find((f) => f.id === quick.fieldId); return row && field && !field.internal && canRead(row, field) && _jsxs(DataViewDialog, { title: field.label, onClose: () => setQuick(null), children: [field.renderQuickView?.({ row, field, value: getFieldValue(row, field), close: () => setQuick(null), openRow: () => { setQuick(null); openRow(row); } }), canEdit(row, field) && _jsx("button", { onClick: () => { setQuick(null); setDetailId(getRowId(row)); setEditing({ rowId: getRowId(row), fieldId: field.id, surface: "detail" }); }, children: "\u7DE8\u8F2F" })] }); })(), detailRow && _jsxs(DataViewDialog, { title: readableLabel(detailRow), busy: busy, onClose: () => { setDetailId(null); setEditing(null); }, children: [props.renderDetail ? props.renderDetail(detailContext(detailRow)) : _jsx("div", { className: "hy-dv-detail", children: availableFields.filter((field) => canRead(detailRow, field)).map((field) => _jsxs("label", { children: [_jsx("span", { children: field.label }), renderCell(detailRow, field, "detail")] }, field.id)) }), features.copy !== false && _jsx("button", { onClick: async () => { try {
694
694
  await navigator.clipboard.writeText(formatReadableRow(detailRow, availableFields));
695
695
  setOperation({ state: "saved", message: "已複製可讀內容" });
696
696
  }
@@ -726,7 +726,10 @@ function DataViewInner(props) {
726
726
  }
727
727
  }
728
728
  function ToolbarButton({ children, active = false, onClick }) {
729
- return _jsx("button", { type: "button", className: active ? "is-active" : "", onClick: onClick, children: children });
729
+ const label = Children.toArray(children).filter((v) => typeof v === "string" || typeof v === "number").join("").trim();
730
+ const icon = label.startsWith("篩選") ? "▽" : label.startsWith("排序") ? "↕" : label.startsWith("分組") ? "▤" : null;
731
+ const count = label.match(/\d+$/)?.[0];
732
+ return _jsxs("button", { type: "button", title: label, "aria-label": label, "aria-pressed": active, className: `${active ? "is-active" : ""} ${icon ? "hy-dv-tool" : ""}`, onClick: onClick, children: [icon && _jsxs("span", { className: "hy-dv-tool-icon", "aria-hidden": true, children: [icon, count && _jsx("small", { children: count })] }), _jsx("span", { className: "hy-dv-tool-label", children: children })] });
730
733
  }
731
734
  function PanelShell({ title, children, onClose }) {
732
735
  return _jsxs("aside", { className: "hy-dv-panel", children: [_jsxs("header", { children: [_jsx("h3", { children: title }), _jsx("button", { type: "button", onClick: onClose, "aria-label": `關閉${title}`, children: "\u00D7" })] }), children] });