@keenaioc/data-view 0.0.0-poc.1 → 0.2.0-alpha.8

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.
Files changed (109) hide show
  1. package/CHANGELOG.md +105 -0
  2. package/DATA-PRIVACY.md +20 -0
  3. package/INTEGRATION.md +178 -0
  4. package/KNOWN-LIMITATIONS.md +34 -0
  5. package/LICENSE +21 -0
  6. package/README.md +67 -5
  7. package/dist/contracts.d.ts +33 -0
  8. package/dist/contracts.d.ts.map +1 -0
  9. package/dist/contracts.js +83 -0
  10. package/dist/contracts.js.map +1 -0
  11. package/dist/data-view.d.ts +3 -0
  12. package/dist/data-view.d.ts.map +1 -0
  13. package/dist/data-view.js +822 -0
  14. package/dist/data-view.js.map +1 -0
  15. package/dist/engine.d.ts +34 -0
  16. package/dist/engine.d.ts.map +1 -0
  17. package/dist/engine.js +307 -0
  18. package/dist/engine.js.map +1 -0
  19. package/dist/gantt-bar.d.ts +17 -0
  20. package/dist/gantt-bar.d.ts.map +1 -0
  21. package/dist/gantt-bar.js +142 -0
  22. package/dist/gantt-bar.js.map +1 -0
  23. package/dist/gantt-dependencies.d.ts +27 -0
  24. package/dist/gantt-dependencies.d.ts.map +1 -0
  25. package/dist/gantt-dependencies.js +218 -0
  26. package/dist/gantt-dependencies.js.map +1 -0
  27. package/dist/gantt-dependency-ui.d.ts +37 -0
  28. package/dist/gantt-dependency-ui.d.ts.map +1 -0
  29. package/dist/gantt-dependency-ui.js +79 -0
  30. package/dist/gantt-dependency-ui.js.map +1 -0
  31. package/dist/gantt-grid.d.ts +27 -0
  32. package/dist/gantt-grid.d.ts.map +1 -0
  33. package/dist/gantt-grid.js +72 -0
  34. package/dist/gantt-grid.js.map +1 -0
  35. package/dist/gantt-operations.d.ts +14 -0
  36. package/dist/gantt-operations.d.ts.map +1 -0
  37. package/dist/gantt-operations.js +103 -0
  38. package/dist/gantt-operations.js.map +1 -0
  39. package/dist/index.d.ts +17 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +17 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/operations.d.ts +11 -0
  44. package/dist/operations.d.ts.map +1 -0
  45. package/dist/operations.js +117 -0
  46. package/dist/operations.js.map +1 -0
  47. package/dist/registry.d.ts +13 -0
  48. package/dist/registry.d.ts.map +1 -0
  49. package/dist/registry.js +465 -0
  50. package/dist/registry.js.map +1 -0
  51. package/dist/remote-data.d.ts +19 -0
  52. package/dist/remote-data.d.ts.map +1 -0
  53. package/dist/remote-data.js +67 -0
  54. package/dist/remote-data.js.map +1 -0
  55. package/dist/row-move.d.ts +7 -0
  56. package/dist/row-move.d.ts.map +1 -0
  57. package/dist/row-move.js +27 -0
  58. package/dist/row-move.js.map +1 -0
  59. package/dist/schedule-view.d.ts +27 -0
  60. package/dist/schedule-view.d.ts.map +1 -0
  61. package/dist/schedule-view.js +122 -0
  62. package/dist/schedule-view.js.map +1 -0
  63. package/dist/schedule.d.ts +28 -0
  64. package/dist/schedule.d.ts.map +1 -0
  65. package/dist/schedule.js +97 -0
  66. package/dist/schedule.js.map +1 -0
  67. package/dist/style.css +986 -0
  68. package/dist/timeline.d.ts +25 -0
  69. package/dist/timeline.d.ts.map +1 -0
  70. package/dist/timeline.js +58 -0
  71. package/dist/timeline.js.map +1 -0
  72. package/dist/toolbar.d.ts +10 -0
  73. package/dist/toolbar.d.ts.map +1 -0
  74. package/dist/toolbar.js +31 -0
  75. package/dist/toolbar.js.map +1 -0
  76. package/dist/types.d.ts +472 -0
  77. package/dist/types.d.ts.map +1 -0
  78. package/dist/types.js +22 -0
  79. package/dist/types.js.map +1 -0
  80. package/dist/use-remote-data.d.ts +18 -0
  81. package/dist/use-remote-data.d.ts.map +1 -0
  82. package/dist/use-remote-data.js +12 -0
  83. package/dist/use-remote-data.js.map +1 -0
  84. package/dist/use-row-move.d.ts +12 -0
  85. package/dist/use-row-move.d.ts.map +1 -0
  86. package/dist/use-row-move.js +151 -0
  87. package/dist/use-row-move.js.map +1 -0
  88. package/dist/v2-panels.d.ts +37 -0
  89. package/dist/v2-panels.d.ts.map +1 -0
  90. package/dist/v2-panels.js +108 -0
  91. package/dist/v2-panels.js.map +1 -0
  92. package/dist/view-state.d.ts +30 -0
  93. package/dist/view-state.d.ts.map +1 -0
  94. package/dist/view-state.js +94 -0
  95. package/dist/view-state.js.map +1 -0
  96. package/dist/view-workspace.d.ts +3 -0
  97. package/dist/view-workspace.d.ts.map +1 -0
  98. package/dist/view-workspace.js +95 -0
  99. package/dist/view-workspace.js.map +1 -0
  100. package/dist/virtual-list.d.ts +12 -0
  101. package/dist/virtual-list.d.ts.map +1 -0
  102. package/dist/virtual-list.js +46 -0
  103. package/dist/virtual-list.js.map +1 -0
  104. package/package.json +19 -16
  105. package/index.d.ts +0 -2
  106. package/index.js +0 -5
  107. package/react.d.ts +0 -2
  108. package/react.js +0 -8
  109. package/style.css +0 -1
@@ -0,0 +1,822 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { Fragment, Children, useEffect, useMemo, useRef, useState, } from "react";
4
+ import { DataViewToolbar } from "./toolbar.js";
5
+ import { buildGroupTree, DataViewConflictError, filterRows, getFieldValue, getPinnedOffsets, getRightPinnedOffsets, MAX_GROUP_LEVELS, moveColumn, normalizeColumnWidth, normalizeViewConfig, retainViewReferences, searchRows, sortRows, validateValue, visibleFields, } from "./engine.js";
6
+ import { FILTER_OPERATOR_LABELS, getFieldTypeDefinition, renderFieldValue, formatClipboardValue, formatReadableValue, formatReadableRow } from "./registry.js";
7
+ import { canWriteField, createPasteChanges, encodeTSV, newOperationId, validateChanges, validateFieldChange } from "./operations.js";
8
+ import { createDataViewQuery, dataViewQueryKey, resolveMetrics, mergeViewState, splitViewState, createWriteQueue, virtualWindow } from "./contracts.js";
9
+ import { VirtualList } from "./virtual-list.js";
10
+ import { useRowMove } from "./use-row-move.js";
11
+ import { createSnapshotWriter } from "./view-state.js";
12
+ import { ScheduleView } from "./schedule-view.js";
13
+ import { applyGanttDependencyChange, ganttHistoryGuard, validateGanttScheduleGuard } from "./gantt-dependencies.js";
14
+ import { BatchEditor, ColorSettings, DataViewDialog, FieldCreator, FieldManager, ViewSettings } from "./v2-panels.js";
15
+ export function DataView(props) {
16
+ return _jsx(DataViewInner, { ...props }, props.identityKey ?? "default");
17
+ }
18
+ function DataViewInner(props) {
19
+ const { fields, rows, getRowId, title = "資料表格", loading = false, error = null, access = "granted", emptyMessage = "目前沒有資料", onViewChange, onCellCommit, onBatchCommit, onFieldChange, ganttDependencies, onGanttDependencyChange, onRequestAttachment, permissions, selectedRowIds, onSelectionChange, searchPlaceholder = "搜尋目前資料", } = props;
20
+ const [internalView, setInternalView] = useState(() => mergeViewState(normalizeViewConfig(fields, props.view ?? props.initialView), props.runtimeState));
21
+ const activeView = internalView;
22
+ const search = activeView.location?.search ?? "";
23
+ const [panel, setPanel] = useState(null);
24
+ const [draggedColumn, setDraggedColumn] = useState(null);
25
+ const [editing, setEditing] = useState(null);
26
+ const [viewSaveState, setViewSaveState] = useState(onViewChange ? "idle" : "readonly");
27
+ const [cellStatuses, setCellStatuses] = useState({});
28
+ const [localSelected, setLocalSelected] = useState([]);
29
+ const viewRef = useRef(activeView);
30
+ const layoutRef = useRef(normalizeViewConfig(fields, props.view ?? props.initialView));
31
+ const saveCallback = useRef(onViewChange);
32
+ useEffect(() => { saveCallback.current = onViewChange; }, [onViewChange]);
33
+ const writer = useRef(null);
34
+ const latestSave = useRef(0);
35
+ const pendingViewWrite = useRef(false);
36
+ const rootRef = useRef(null);
37
+ const scrollRef = useRef(null);
38
+ const mobileRef = useRef(null);
39
+ useEffect(() => { setEditing(null); }, [props.presentation]);
40
+ const [scrollTop, setScrollTop] = useState(0);
41
+ const [focus, setFocus] = useState(null);
42
+ const [anchor, setAnchor] = useState(null);
43
+ const [detailId, setDetailId] = useState(null);
44
+ const [dialog, setDialog] = useState(null);
45
+ const [pending, setPending] = useState(null);
46
+ const [operation, setOperation] = useState({ state: "idle" });
47
+ const [undoStack, setUndoStack] = useState([]);
48
+ const [redoStack, setRedoStack] = useState([]);
49
+ const busyRef = useRef(false);
50
+ const pendingCells = useRef(new Set());
51
+ const writes = useRef(createWriteQueue());
52
+ useEffect(() => { const queue = createWriteQueue(); writes.current = queue; return () => queue.dispose(); }, []);
53
+ const [quick, setQuick] = useState(null);
54
+ const bottomRef = useRef(null);
55
+ const [viewportHeight, setViewportHeight] = useState(560);
56
+ const [viewportWidth, setViewportWidth] = useState(0);
57
+ const metrics = resolveMetrics(activeView.density, props.metrics);
58
+ const runtimeCallback = useRef(props.onRuntimeChange);
59
+ runtimeCallback.current = props.onRuntimeChange;
60
+ const detailCallback = useRef(props.onViewChangeDetail);
61
+ detailCallback.current = props.onViewChangeDetail;
62
+ const runtimeWriter = useRef(createSnapshotWriter((v) => runtimeCallback.current?.(v)));
63
+ const detailWriter = useRef(createSnapshotWriter((v) => detailCallback.current?.(v)));
64
+ const features = props.features ?? {};
65
+ const availableFields = useMemo(() => fields.filter((f) => !f.internal), [fields]);
66
+ const latest = useRef({ rows, fields, permissions, access, ganttDependencies, mapping: activeView.gantt });
67
+ useEffect(() => { latest.current = { rows, fields, permissions, access, ganttDependencies, mapping: activeView.gantt }; }, [rows, fields, permissions, access, ganttDependencies, activeView.gantt]);
68
+ useEffect(() => {
69
+ if (props.view)
70
+ layoutRef.current = normalizeViewConfig(fields, props.view);
71
+ setInternalView((current) => mergeViewState(normalizeViewConfig(fields, !pendingViewWrite.current && props.view ? props.view : current), props.runtimeState));
72
+ }, [fields, props.view, props.runtimeState]);
73
+ useEffect(() => { viewRef.current = activeView; }, [activeView]);
74
+ useEffect(() => {
75
+ if (mobileRef.current)
76
+ mobileRef.current.scrollTop = viewRef.current.location?.mobileTop ?? 0;
77
+ const el = scrollRef.current;
78
+ if (!el)
79
+ return;
80
+ el.scrollTop = viewRef.current.location?.tableTop ?? 0;
81
+ el.scrollLeft = viewRef.current.location?.tableLeft ?? 0;
82
+ setScrollTop(el.scrollTop);
83
+ }, [activeView.viewType]);
84
+ useEffect(() => { const leave = (e) => { if (pendingViewWrite.current) {
85
+ e.preventDefault();
86
+ e.returnValue = "";
87
+ } }; window.addEventListener("beforeunload", leave); return () => window.removeEventListener("beforeunload", leave); }, []);
88
+ const selected = selectedRowIds ?? localSelected;
89
+ const selectable = permissions?.canSelectRows !== false;
90
+ const controlColumn = selectable || !!props.rowMove || features.rowOpen === true;
91
+ const controlWidth = (selectable ? 32 : 0) + (props.rowMove ? 24 : 0) + (features.rowOpen === true ? 28 : 0);
92
+ const move = useRowMove(props, activeView, rootRef, (id, action) => writes.current.run([id], action));
93
+ const configurable = permissions?.canConfigureView !== false;
94
+ const canManageFields = Boolean(onFieldChange && permissions?.canManageFields === true && access === "granted");
95
+ const addColumnWidth = canManageFields ? 56 : 0;
96
+ const busy = operation.state === "saving";
97
+ const selectedSet = useMemo(() => new Set(selected), [selected]);
98
+ const selectedRows = useMemo(() => rows.filter((row) => selectedSet.has(getRowId(row))), [rows, selectedSet, getRowId]);
99
+ const detailRow = detailId ? rows.find((row) => getRowId(row) === detailId) : undefined;
100
+ function setView(next, scope = "layout") {
101
+ if (scope === "layout" && !configurable)
102
+ return;
103
+ const normalized = retainViewReferences(viewRef.current, normalizeViewConfig(fields, next));
104
+ viewRef.current = normalized;
105
+ setInternalView(normalized);
106
+ const runtime = { viewType: normalized.viewType, location: normalized.location, collapsedGroups: normalized.collapsedGroups, ...(props.runtimeState?.filters || scope === "query" ? { filters: normalized.filters, sorts: normalized.sorts } : {}) };
107
+ const callbacks = [];
108
+ if (detailCallback.current)
109
+ callbacks.push(detailWriter.current({ scope, view: normalized, runtime }));
110
+ if (scope !== "layout" && runtimeCallback.current)
111
+ callbacks.push(runtimeWriter.current(runtime));
112
+ else if (saveCallback.current) {
113
+ if (!writer.current)
114
+ writer.current = createSnapshotWriter((value) => saveCallback.current?.(value));
115
+ const savedLayout = runtimeCallback.current ? { ...splitViewState(normalized).layout, filters: layoutRef.current.filters, sorts: layoutRef.current.sorts } : normalized;
116
+ layoutRef.current = savedLayout;
117
+ callbacks.push(writer.current(savedLayout));
118
+ }
119
+ if (!callbacks.length)
120
+ return;
121
+ setViewSaveState("saving");
122
+ pendingViewWrite.current = true;
123
+ const saveNumber = ++latestSave.current;
124
+ void Promise.all(callbacks).then(() => {
125
+ if (latestSave.current === saveNumber) {
126
+ setViewSaveState("saved");
127
+ pendingViewWrite.current = false;
128
+ }
129
+ }, (e) => { if (latestSave.current === saveNumber)
130
+ setViewSaveState(e instanceof DataViewConflictError ? "conflict" : "failed"); });
131
+ }
132
+ function patchView(patch, scope = "layout") { setView({ ...viewRef.current, ...patch }, scope); }
133
+ function patchLocation(patch) { patchView({ location: { ...viewRef.current.location, ...patch } }, "navigation"); }
134
+ const query = useMemo(() => createDataViewQuery(activeView), [activeView.filters, activeView.sorts, activeView.groups, search]);
135
+ const queryKey = dataViewQueryKey(query);
136
+ const remoteStale = !!props.remote && props.remote.queryKey !== queryKey;
137
+ const queryCallback = useRef(props.onQueryChange);
138
+ queryCallback.current = props.onQueryChange;
139
+ const previousQuery = useRef(queryKey);
140
+ useEffect(() => { if (access === "granted" && !loading && !error)
141
+ queryCallback.current?.(query); }, [queryKey, props.onQueryChange, access, loading, error]);
142
+ useEffect(() => { if (previousQuery.current === queryKey)
143
+ return; previousQuery.current = queryKey; setLocalSelected([]); onSelectionChange?.([]); setFocus(null); setAnchor(null); setEditing(null); setUndoStack([]); setRedoStack([]); }, [queryKey]);
144
+ const canFilter = permissions?.canFilter ?? configurable;
145
+ const canSort = permissions?.canSort ?? configurable;
146
+ const processedRows = useMemo(() => {
147
+ if (props.remote)
148
+ return remoteStale ? [] : rows;
149
+ const searched = searchRows(rows, fields, search);
150
+ const filtered = filterRows(searched, fields, activeView.filters.rules, activeView.filters.conjunction);
151
+ return sortRows(filtered, fields, activeView.sorts);
152
+ }, [activeView.filters, activeView.sorts, fields, rows, search, Boolean(props.remote), remoteStale]);
153
+ const fullGroups = useMemo(() => buildGroupTree(processedRows, fields, activeView.groups), [processedRows, fields, activeView.groups]);
154
+ const groups = fullGroups;
155
+ const visible = useMemo(() => visibleFields(fields, activeView), [activeView, fields]);
156
+ const tableColumnCount = visible.length + (controlColumn ? 1 : 0) + (canManageFields ? 1 : 0);
157
+ const pinnedOffsets = useMemo(() => getPinnedOffsets(activeView.columns, controlWidth), [activeView.columns, selectable]);
158
+ const ganttLeftOffsets = useMemo(() => getPinnedOffsets(activeView.columns, 44), [activeView.columns]);
159
+ const ganttRightOffsets = useMemo(() => getRightPinnedOffsets(activeView.columns), [activeView.columns]);
160
+ const tableWidth = visible.reduce((total, item) => total + item.column.width, (controlWidth) + addColumnWidth);
161
+ const collapsed = useMemo(() => new Set(activeView.collapsedGroups), [activeView.collapsedGroups]);
162
+ const allGroupKeys = useMemo(() => collectGroupKeys(fullGroups), [fullGroups]);
163
+ const rightOffsets = useMemo(() => new Map([...getRightPinnedOffsets(activeView.columns)].map(([id, offset]) => [id, offset + addColumnWidth])), [activeView.columns, addColumnWidth]);
164
+ const virtual = activeView.virtualize !== false || processedRows.length > 200;
165
+ const rowHeight = metrics.rowHeight;
166
+ const flatEntries = useMemo(() => flattenGroups(fullGroups, collapsed), [fullGroups, collapsed]);
167
+ const desktopRows = useMemo(() => flatEntries.flatMap((entry) => entry.kind === "row" ? [entry.row] : []), [flatEntries]);
168
+ const entryHeights = useMemo(() => flatEntries.map((e) => e.kind === "group" ? metrics.groupHeight + (e.node.depth ? metrics.nestedGroupGap : metrics.groupGap) : rowHeight), [flatEntries, rowHeight, metrics.groupHeight, metrics.groupGap, metrics.nestedGroupGap]);
169
+ const windowed = useMemo(() => virtualWindow(entryHeights, scrollTop, viewportHeight), [entryHeights, scrollTop, viewportHeight]);
170
+ const virtualStart = virtual ? windowed.start : 0;
171
+ const virtualEnd = virtual ? windowed.end : flatEntries.length;
172
+ useEffect(() => {
173
+ const el = scrollRef.current;
174
+ if (!el)
175
+ return;
176
+ const observer = new ResizeObserver(() => { setViewportHeight(el.clientHeight); setViewportWidth(el.clientWidth); });
177
+ observer.observe(el);
178
+ return () => observer.disconnect();
179
+ }, [activeView.viewType, loading, error, access, processedRows.length === 0]);
180
+ const mobileFields = useMemo(() => activeView.mobile ? activeView.mobile.fieldIds.flatMap((id) => { const field = availableFields.find((f) => f.id === id); return field ? [field] : []; }) : visible.slice(0, 5).map((v) => v.field), [activeView.mobile, availableFields, visible]);
181
+ function readableLabel(row) {
182
+ const id = activeView.viewType === "gantt" ? activeView.gantt?.titleFieldId : activeView.viewType === "calendar" ? activeView.calendar?.titleFieldId : activeView.mobile?.summaryFieldId;
183
+ const field = fields.find((f) => f.id === id) ?? availableFields.find((f) => !f.renderAction && canRead(row, f));
184
+ if (field && (field.internal || !canRead(row, field)))
185
+ return "無權限";
186
+ return props.getRowLabel?.(row)?.trim() || (field ? formatReadableValue(getFieldValue(row, field), { ...field, internal: false }, row) : "") || "未命名工作";
187
+ }
188
+ function openRow(row) { if (features.rowOpen === false)
189
+ return; if (props.onRowOpen)
190
+ props.onRowOpen(row);
191
+ else
192
+ setDetailId(getRowId(row)); }
193
+ function groupLabel(node) {
194
+ const field = fields.find((f) => f.id === node.fieldId);
195
+ return _jsxs(_Fragment, { children: [_jsx("strong", { className: "hy-dv-group-label", style: { backgroundColor: node.color, color: node.textColor }, children: node.valueKey === "__restricted__" ? "無權限" : field?.renderGroup?.(node) ?? node.label }), _jsxs("span", { children: [props.remote?.groupCounts?.[node.key] ?? node.rows.length, props.remote && !props.remote.complete && props.remote.groupCounts?.[node.key] === undefined ? " 已載入" : ""] })] });
196
+ }
197
+ function detailContext(row) { return { row, fields: availableFields.filter((f) => canRead(row, f)), close: () => { setDetailId(null); setEditing(null); }, renderCell: (field) => renderCell(row, field, "detail") }; }
198
+ function loadNearEnd(el) { if (el.scrollHeight - el.scrollTop - el.clientHeight < 200 && props.remote && !remoteStale && !props.remote.complete && !props.remote.loading && !props.remote.error)
199
+ props.remote.onLoadMore?.(); }
200
+ const colorMap = useMemo(() => {
201
+ const result = new Map();
202
+ for (const row of rows) {
203
+ const colors = new Map();
204
+ for (const rule of activeView.colorRules ?? []) {
205
+ const field = fields.find((f) => f.id === rule.condition.fieldId);
206
+ if (!field || field.canRead?.({ row, field }) === false || colors.has(rule.target))
207
+ continue;
208
+ if (getFieldTypeDefinition(field.type).filter(getFieldValue(row, field), rule.condition, field))
209
+ colors.set(rule.target, rule.color);
210
+ }
211
+ if (colors.size)
212
+ result.set(getRowId(row), colors);
213
+ }
214
+ return result;
215
+ }, [rows, fields, getRowId, activeView.colorRules]);
216
+ function changeSelection(next) {
217
+ const unique = [...new Set(next)];
218
+ if (!selectedRowIds)
219
+ setLocalSelected(unique);
220
+ onSelectionChange?.(unique);
221
+ }
222
+ function toggleRow(rowId) {
223
+ changeSelection(selected.includes(rowId) ? selected.filter((id) => id !== rowId) : [...selected, rowId]);
224
+ }
225
+ const pageIds = processedRows.map(getRowId);
226
+ const allPageSelected = pageIds.length > 0 && pageIds.every((id) => selected.includes(id));
227
+ function togglePageSelection() {
228
+ if (allPageSelected)
229
+ changeSelection(selected.filter((id) => !pageIds.includes(id)));
230
+ else
231
+ changeSelection([...selected, ...pageIds]);
232
+ }
233
+ function toggleGroup(key) {
234
+ if (permissions?.canCollapseGroups === false)
235
+ return;
236
+ const next = new Set(activeView.collapsedGroups);
237
+ if (next.has(key))
238
+ next.delete(key);
239
+ else
240
+ next.add(key);
241
+ patchView({ collapsedGroups: [...next] }, "navigation");
242
+ }
243
+ function setAllGroups(close) {
244
+ if (permissions?.canCollapseGroups !== false)
245
+ patchView({ collapsedGroups: close ? allGroupKeys : [] }, "navigation");
246
+ }
247
+ function resizeColumn(fieldId, startWidth, event) {
248
+ event.preventDefault();
249
+ const startX = event.clientX;
250
+ function move(pointer) {
251
+ const width = normalizeColumnWidth(startWidth + pointer.clientX - startX);
252
+ setView({
253
+ ...viewRef.current,
254
+ columns: viewRef.current.columns.map((column) => column.fieldId === fieldId ? { ...column, width } : column),
255
+ });
256
+ }
257
+ function up() {
258
+ window.removeEventListener("pointermove", move);
259
+ window.removeEventListener("pointerup", up);
260
+ window.removeEventListener("pointercancel", up);
261
+ }
262
+ window.addEventListener("pointermove", move);
263
+ window.addEventListener("pointerup", up);
264
+ window.addEventListener("pointercancel", up);
265
+ }
266
+ function dropColumn(targetId) {
267
+ if (!draggedColumn)
268
+ return;
269
+ patchView({ columns: moveColumn(activeView.columns, draggedColumn, targetId) });
270
+ setDraggedColumn(null);
271
+ }
272
+ function canRead(row, field) {
273
+ return field.canRead?.({ row, field }) !== false;
274
+ }
275
+ function canEdit(row, field) {
276
+ const spec = getFieldTypeDefinition(field.type);
277
+ return Boolean(!busyRef.current && !pendingCells.current.has(`${getRowId(row)}:${field.id}`) && access === "granted" && !remoteStale && (field.Editor || spec.Editor) && canWriteField(row, field, permissions) &&
278
+ (field.type === "attachment" ? onRequestAttachment : onBatchCommit ?? onCellCommit));
279
+ }
280
+ async function commitCell(row, field, nextValue) {
281
+ if (!canEdit(row, field))
282
+ return;
283
+ const rowId = getRowId(row);
284
+ const key = `${rowId}:${field.id}`;
285
+ const previousValue = getFieldValue(row, field);
286
+ if (JSON.stringify(previousValue) === JSON.stringify(nextValue)) {
287
+ setEditing(null);
288
+ return;
289
+ }
290
+ const validation = validateValue(nextValue, field, row);
291
+ if (validation) {
292
+ setEditing(null);
293
+ setCellStatuses((current) => ({ ...current, [key]: { state: "failed", message: validation } }));
294
+ return;
295
+ }
296
+ setEditing(null);
297
+ if (onBatchCommit && !onCellCommit) {
298
+ await execute({ id: newOperationId(), reason: "edit", changes: [{ row, rowId, field, previousValue, nextValue }] });
299
+ return;
300
+ }
301
+ pendingCells.current.add(key);
302
+ setCellStatuses((current) => ({ ...current, [key]: { state: "saving" } }));
303
+ try {
304
+ await writes.current.run([rowId], async () => {
305
+ const fresh = latest.current.rows.find((r) => getRowId(r) === rowId);
306
+ const definition = latest.current.fields.find((f) => f.id === field.id);
307
+ if (!fresh || !definition || latest.current.access !== "granted" || !canWriteField(fresh, definition, latest.current.permissions))
308
+ throw new Error("資料不存在或無修改權限");
309
+ if (JSON.stringify(getFieldValue(fresh, definition)) !== JSON.stringify(previousValue))
310
+ throw new DataViewConflictError();
311
+ await onCellCommit?.({ row: fresh, rowId, field: definition, previousValue, nextValue });
312
+ });
313
+ setCellStatuses((current) => ({ ...current, [key]: { state: "saved" } }));
314
+ setTimeout(() => setCellStatuses((current) => {
315
+ if (current[key]?.state !== "saved")
316
+ return current;
317
+ const next = { ...current };
318
+ delete next[key];
319
+ return next;
320
+ }), 1400);
321
+ }
322
+ catch (commitError) {
323
+ setCellStatuses((current) => ({
324
+ ...current,
325
+ [key]: {
326
+ state: commitError instanceof DataViewConflictError ? "conflict" : "failed",
327
+ message: commitError instanceof Error ? commitError.message : "儲存失敗",
328
+ },
329
+ }));
330
+ }
331
+ finally {
332
+ pendingCells.current.delete(key);
333
+ }
334
+ }
335
+ function renderCell(row, field, surface) {
336
+ if (field.internal)
337
+ return null;
338
+ if (!canRead(row, field))
339
+ return _jsx("span", { className: "hy-dv-no-access", children: "\u7121\u6B0A\u9650" });
340
+ const rowId = getRowId(row);
341
+ const key = `${rowId}:${field.id}`;
342
+ const value = getFieldValue(row, field);
343
+ const status = cellStatuses[key];
344
+ const isEditing = editing?.rowId === rowId && editing.fieldId === field.id && editing.surface === surface;
345
+ const spec = getFieldTypeDefinition(field.type);
346
+ if (field.renderAction)
347
+ return _jsxs("fieldset", { className: "hy-dv-action-cell", disabled: field.canAct?.(row) === false || status?.state === "saving" || access !== "granted", children: [field.renderAction({ row, field, value, close: () => setEditing(null), openRow: () => openRow(row), disabled: field.canAct?.(row) === false || status?.state === "saving", pending: status?.state === "saving", run: async (action) => { if (field.canAct?.(row) === false || pendingCells.current.has(key))
348
+ return; pendingCells.current.add(key); setCellStatuses((s) => ({ ...s, [key]: { state: "saving" } })); try {
349
+ await writes.current.run([rowId], () => { const fresh = latest.current.rows.find((r) => getRowId(r) === rowId); const f = latest.current.fields.find((f) => f.id === field.id); if (!fresh || !f || latest.current.access !== "granted" || f.canRead?.({ row: fresh, field: f }) === false || f.canAct?.(fresh) === false)
350
+ throw new Error("無操作權限"); return action(); });
351
+ setCellStatuses((s) => ({ ...s, [key]: { state: "saved" } }));
352
+ }
353
+ catch (e) {
354
+ setCellStatuses((s) => ({ ...s, [key]: { state: e instanceof DataViewConflictError ? "conflict" : "failed", message: e instanceof Error ? e.message : "操作失敗" } }));
355
+ }
356
+ finally {
357
+ pendingCells.current.delete(key);
358
+ } } }), status && _jsx(SaveBadge, { state: status.state, message: status.message })] });
359
+ if (isEditing && !spec.toggleValue && (field.Editor || spec.Editor) && canEdit(row, field)) {
360
+ const Editor = field.Editor ?? spec.Editor;
361
+ return _jsx("div", { className: "hy-dv-editor-wrap", children: _jsx(Editor, { value: value, row: row, field: field, onCommit: (next) => void commitCell(row, field, next), onCancel: () => setEditing(null), onRequestAttachment: onRequestAttachment ? async (files) => {
362
+ if (pendingCells.current.has(key))
363
+ return;
364
+ pendingCells.current.add(key);
365
+ setEditing(null);
366
+ setCellStatuses((current) => ({ ...current, [key]: { state: "saving" } }));
367
+ try {
368
+ await writes.current.run([rowId], () => onRequestAttachment({ row, rowId, field, files }));
369
+ setCellStatuses((current) => ({ ...current, [key]: { state: "saved" } }));
370
+ }
371
+ catch (uploadError) {
372
+ setCellStatuses((current) => ({ ...current, [key]: { state: "failed", message: uploadError instanceof Error ? uploadError.message : "附件儲存失敗" } }));
373
+ }
374
+ finally {
375
+ pendingCells.current.delete(key);
376
+ }
377
+ } : undefined }) });
378
+ }
379
+ if (field.type === "attachment")
380
+ return _jsxs("div", { className: "hy-dv-attachment-cell", children: [renderFieldValue(value, field, row), canEdit(row, field) && _jsx("button", { type: "button", onClick: () => setEditing({ rowId, fieldId: field.id, surface }), children: "\u65B0\u589E\u9644\u4EF6" }), status && _jsx(SaveBadge, { state: status.state, message: status.message })] });
381
+ return _jsxs("button", { type: "button", className: `hy-dv-cell-button ${canEdit(row, field) ? "is-editable" : ""} ${spec.toggleValue ? "is-toggle" : ""}`, role: spec.toggleValue ? "checkbox" : undefined, "aria-checked": spec.toggleValue ? Boolean(value) : undefined, "aria-label": spec.toggleValue ? `${rowId} ${field.label}` : undefined, "aria-busy": status?.state === "saving", tabIndex: surface === "desktop" ? -1 : undefined, onClick: (event) => {
382
+ if (event.shiftKey && surface === "desktop")
383
+ return;
384
+ setFocus({ rowId, fieldId: field.id });
385
+ setAnchor(null);
386
+ if (spec.toggleValue) {
387
+ void commitCell(row, field, spec.toggleValue(value));
388
+ return;
389
+ }
390
+ if (field.renderQuickView) {
391
+ setQuick({ rowId, fieldId: field.id });
392
+ return;
393
+ }
394
+ if (!canEdit(row, field)) {
395
+ openRow(row);
396
+ return;
397
+ }
398
+ if (surface !== "detail" && (field.Editor || ["multiLineText", "multiSelect", "attachment", "relation", "person"].includes(field.type))) {
399
+ setDetailId(rowId);
400
+ setEditing({ rowId, fieldId: field.id, surface: "detail" });
401
+ return;
402
+ }
403
+ if (canEdit(row, field))
404
+ setEditing({ rowId, fieldId: field.id, surface });
405
+ }, disabled: status?.state === "saving" || (!!spec.toggleValue && !canEdit(row, field)), title: status?.message, children: [_jsx("span", { className: "hy-dv-cell-value", title: formatReadableValue(value, field, row), children: renderFieldValue(value, field, row) }), field.appearance?.arrow === true && !spec.toggleValue && _jsx("span", { className: "hy-dv-cell-arrow", "aria-hidden": true, children: "\u2304" }), status && _jsx(SaveBadge, { state: status.state, message: status.message })] });
406
+ }
407
+ function reportFailure(error) {
408
+ setOperation({ state: error instanceof DataViewConflictError ? "conflict" : "failed", message: error instanceof Error ? error.message : String(error) });
409
+ }
410
+ async function execute(request) {
411
+ if (!onBatchCommit || busyRef.current || latest.current.access !== "granted" || remoteStale)
412
+ return false;
413
+ const keys = request.changes.map((c) => `${c.rowId}:${c.field.id}`);
414
+ keys.forEach((k) => pendingCells.current.add(k));
415
+ setCellStatuses((s) => ({ ...s, ...Object.fromEntries(keys.map((k) => [k, { state: "saving" }])) }));
416
+ return writes.current.run(request.changes.map((c) => c.rowId), async () => {
417
+ const currentRows = new Map(latest.current.rows.map((row) => [getRowId(row), row]));
418
+ try {
419
+ if (request.scheduleGuard) {
420
+ const guard = request.scheduleGuard;
421
+ const mapping = latest.current.mapping;
422
+ if (!mapping || mapping.startFieldId !== guard.startFieldId || mapping.endFieldId !== guard.endFieldId || (mapping.workdaysOnly === true) !== guard.workdaysOnly)
423
+ throw new DataViewConflictError("日期設定已變更,請重新預覽");
424
+ validateGanttScheduleGuard(guard, latest.current.rows, latest.current.fields, getRowId, latest.current.ganttDependencies);
425
+ for (const c of request.changes) {
426
+ const row = currentRows.get(c.rowId);
427
+ if (row && latest.current.ganttDependencies?.isDateLocked?.(row))
428
+ throw new DataViewConflictError("排程已確認或鎖定,請重新協調日期");
429
+ }
430
+ }
431
+ const changes = request.changes.map((change) => {
432
+ const row = currentRows.get(change.rowId);
433
+ const field = latest.current.fields.find((f) => f.id === change.field.id);
434
+ if (!row || !field)
435
+ throw new DataViewConflictError("資料列或欄位已不存在,請重新選取");
436
+ if (JSON.stringify(getFieldValue(row, field)) !== JSON.stringify(change.previousValue))
437
+ throw new DataViewConflictError("預覽後資料已更新,請重新檢查修改內容");
438
+ return { ...change, row, field };
439
+ });
440
+ const validation = validateChanges(changes, latest.current.permissions);
441
+ if (validation)
442
+ throw new Error(validation);
443
+ setOperation({ state: "saving", message: `正在儲存 ${changes.length} 格` });
444
+ await onBatchCommit({ ...request, changes });
445
+ if (features.undo !== false && request.reason !== "undo" && request.reason !== "redo") {
446
+ setUndoStack((stack) => [...stack.slice(-19), { changes: changes.map((c) => ({ ...c, previousValue: structuredClone(c.previousValue), nextValue: structuredClone(c.nextValue) })), scheduleGuard: request.scheduleGuard ? structuredClone(request.scheduleGuard) : undefined }]);
447
+ setRedoStack([]);
448
+ }
449
+ setCellStatuses((s) => ({ ...s, ...Object.fromEntries(keys.map((k) => [k, { state: "saved" }])) }));
450
+ setOperation({ state: "saved", message: `已儲存 ${changes.length} 格` });
451
+ return true;
452
+ }
453
+ catch (error) {
454
+ setCellStatuses((s) => ({ ...s, ...Object.fromEntries(keys.map((k) => [k, { state: error instanceof DataViewConflictError ? "conflict" : "failed", message: error instanceof Error ? error.message : "儲存失敗" }])) }));
455
+ reportFailure(error);
456
+ return false;
457
+ }
458
+ finally {
459
+ keys.forEach((k) => pendingCells.current.delete(k));
460
+ }
461
+ });
462
+ }
463
+ function reviewChanges(changes, reason) {
464
+ const validation = validateChanges(changes, permissions);
465
+ if (validation) {
466
+ reportFailure(new Error(validation));
467
+ return;
468
+ }
469
+ setPending({ id: newOperationId(), reason, changes });
470
+ setDialog("review");
471
+ setEditing(null);
472
+ }
473
+ async function applyFieldChange(change) {
474
+ if (!onFieldChange || busyRef.current || latest.current.access !== "granted")
475
+ return false;
476
+ const validation = validateFieldChange(latest.current.fields, change, latest.current.permissions);
477
+ if (validation) {
478
+ reportFailure(new Error(validation));
479
+ return false;
480
+ }
481
+ busyRef.current = true;
482
+ setOperation({ state: "saving", message: "正在儲存欄位結構" });
483
+ try {
484
+ await writes.current.run(["*"], () => onFieldChange(change));
485
+ setUndoStack([]);
486
+ setRedoStack([]);
487
+ setFocus(null);
488
+ setAnchor(null);
489
+ setEditing(null);
490
+ setOperation({ state: "saved", message: "欄位結構已儲存" });
491
+ return true;
492
+ }
493
+ catch (error) {
494
+ reportFailure(error);
495
+ return false;
496
+ }
497
+ finally {
498
+ busyRef.current = false;
499
+ }
500
+ }
501
+ async function changeDependency(change) {
502
+ const current = latest.current;
503
+ if (!onGanttDependencyChange || !current.ganttDependencies || busyRef.current || current.access !== "granted" || current.permissions?.canManageDependencies !== true)
504
+ return false;
505
+ try {
506
+ applyGanttDependencyChange(current.ganttDependencies, change);
507
+ const link = change.action === "add" ? change.link : current.ganttDependencies.links.find((l) => l.id === change.linkId);
508
+ if (!link || ![link.predecessorId, link.successorId].every((id) => {
509
+ const row = current.rows.find((r) => getRowId(r) === id);
510
+ return row && [current.mapping?.titleFieldId, current.mapping?.startFieldId, current.mapping?.endFieldId].every((fieldId) => { const field = current.fields.find((f) => f.id === fieldId); return field && field.canRead?.({ row, field }) !== false; });
511
+ }))
512
+ throw new Error("相關資料不存在或無權限");
513
+ busyRef.current = true;
514
+ setOperation({ state: "saving", message: "正在儲存依賴" });
515
+ await writes.current.run(["*"], () => onGanttDependencyChange(change));
516
+ setUndoStack([]);
517
+ setRedoStack([]);
518
+ setOperation({ state: "saved", message: "依賴已儲存,日期保持原值" });
519
+ return true;
520
+ }
521
+ catch (error) {
522
+ reportFailure(error);
523
+ return false;
524
+ }
525
+ finally {
526
+ busyRef.current = false;
527
+ }
528
+ }
529
+ async function historyAction(direction) {
530
+ if (features[direction] === false)
531
+ return;
532
+ const stack = direction === "undo" ? undoStack : redoStack;
533
+ const entry = stack.at(-1);
534
+ if (!entry)
535
+ return;
536
+ const changes = direction === "undo" ? entry.changes.map((c) => ({ ...c, previousValue: c.nextValue, nextValue: c.previousValue })) : entry.changes;
537
+ if (await execute({ id: newOperationId(), reason: direction, changes, scheduleGuard: ganttHistoryGuard(entry.scheduleGuard, entry.changes, direction === "undo") })) {
538
+ if (direction === "undo") {
539
+ setUndoStack((s) => s.slice(0, -1));
540
+ setRedoStack((s) => [...s, entry]);
541
+ }
542
+ else {
543
+ setRedoStack((s) => s.slice(0, -1));
544
+ setUndoStack((s) => [...s, entry]);
545
+ }
546
+ }
547
+ }
548
+ function gridRange() {
549
+ if (!focus)
550
+ return null;
551
+ const row = desktopRows.findIndex((r) => getRowId(r) === focus.rowId);
552
+ const col = visible.findIndex((v) => v.field.id === focus.fieldId);
553
+ if (row < 0 || col < 0)
554
+ return null;
555
+ const anchorRow = anchor ? desktopRows.findIndex((r) => getRowId(r) === anchor.rowId) : row;
556
+ const anchorCol = anchor ? visible.findIndex((v) => v.field.id === anchor.fieldId) : col;
557
+ return { row, col, fromRow: Math.min(row, anchorRow < 0 ? row : anchorRow), toRow: Math.max(row, anchorRow), fromCol: Math.min(col, anchorCol < 0 ? col : anchorCol), toCol: Math.max(col, anchorCol) };
558
+ }
559
+ function inputTarget(target) { return target instanceof HTMLElement && (target.matches("input,textarea,select,a") || !!target.closest(".hy-dv-action-cell") || target.isContentEditable); }
560
+ function handleCopy(event) {
561
+ if (features.copy === false || inputTarget(event.target) || !event.target || !isGridTarget(event.target))
562
+ return;
563
+ const range = gridRange();
564
+ if (!range)
565
+ return;
566
+ const values = desktopRows.slice(range.fromRow, range.toRow + 1).map((row) => visible.slice(range.fromCol, range.toCol + 1).map(({ field }) => !field.internal && !field.renderAction && canRead(row, field) ? formatClipboardValue(getFieldValue(row, field), field) : ""));
567
+ event.preventDefault();
568
+ event.clipboardData.setData("text/plain", encodeTSV(values));
569
+ setOperation({ state: "saved", message: `已複製 ${values.length} 列(未讀取無權限值)` });
570
+ }
571
+ function handlePaste(event) {
572
+ if (features.paste === false || inputTarget(event.target) || !isGridTarget(event.target) || !onBatchCommit || busy)
573
+ return;
574
+ const range = gridRange();
575
+ if (!range)
576
+ return;
577
+ event.preventDefault();
578
+ try {
579
+ reviewChanges(createPasteChanges(event.clipboardData.getData("text/plain"), desktopRows, visible.map((v) => v.field), getRowId, range.fromRow, range.fromCol), "paste");
580
+ }
581
+ catch (error) {
582
+ reportFailure(error);
583
+ }
584
+ }
585
+ function isGridTarget(target) { return target instanceof Element && !!target.closest("td[data-hy-cell]"); }
586
+ function handleKey(event) {
587
+ if (inputTarget(event.target) || !isGridTarget(event.target) || editing)
588
+ return;
589
+ const modifier = event.ctrlKey || event.metaKey;
590
+ if (modifier && (event.key.toLowerCase() === "z" || event.key.toLowerCase() === "y")) {
591
+ if (onBatchCommit) {
592
+ event.preventDefault();
593
+ void historyAction(event.shiftKey || event.key.toLowerCase() === "y" ? "redo" : "undo");
594
+ }
595
+ return;
596
+ }
597
+ const range = gridRange();
598
+ if (!range)
599
+ return;
600
+ if (event.key === "Escape") {
601
+ setAnchor(null);
602
+ setEditing(null);
603
+ return;
604
+ }
605
+ if (event.key === " " && modifier && selectable) {
606
+ event.preventDefault();
607
+ toggleRow(getRowId(desktopRows[range.row]));
608
+ return;
609
+ }
610
+ if (event.key === "Enter" || event.key === "F2" || (event.key === " " && !modifier && !event.shiftKey && getFieldTypeDefinition(visible[range.col].field.type).toggleValue)) {
611
+ event.preventDefault();
612
+ const row = desktopRows[range.row];
613
+ const field = visible[range.col].field;
614
+ const toggleValue = getFieldTypeDefinition(field.type).toggleValue;
615
+ if (toggleValue)
616
+ void commitCell(row, field, toggleValue(getFieldValue(row, field)));
617
+ else if (!canEdit(row, field))
618
+ openRow(row);
619
+ else if (field.Editor || ["person", "relation", "multiLineText", "multiSelect"].includes(field.type)) {
620
+ setDetailId(getRowId(row));
621
+ setEditing({ rowId: getRowId(row), fieldId: field.id, surface: "detail" });
622
+ }
623
+ else if (canEdit(row, field))
624
+ setEditing({ rowId: getRowId(row), fieldId: field.id, surface: "desktop" });
625
+ return;
626
+ }
627
+ if ((event.key === "Delete" || event.key === "Backspace") && onBatchCommit && features.clear !== false) {
628
+ event.preventDefault();
629
+ reviewChanges(desktopRows.slice(range.fromRow, range.toRow + 1).flatMap((row) => visible.slice(range.fromCol, range.toCol + 1).map(({ field }) => ({ row, rowId: getRowId(row), field, previousValue: getFieldValue(row, field), nextValue: field.type === "checkbox" ? false : null }))), "clear");
630
+ return;
631
+ }
632
+ let row = range.row;
633
+ let col = range.col;
634
+ if (event.key === "ArrowDown")
635
+ row++;
636
+ else if (event.key === "ArrowUp")
637
+ row--;
638
+ else if (event.key === "PageDown")
639
+ row += Math.max(1, Math.floor(viewportHeight / rowHeight));
640
+ else if (event.key === "PageUp")
641
+ row -= Math.max(1, Math.floor(viewportHeight / rowHeight));
642
+ else if (event.key === "ArrowLeft")
643
+ col--;
644
+ else if (event.key === "ArrowRight")
645
+ col++;
646
+ else if (event.key === "Home") {
647
+ col = 0;
648
+ if (modifier)
649
+ row = 0;
650
+ }
651
+ else if (event.key === "End") {
652
+ col = visible.length - 1;
653
+ if (modifier)
654
+ row = desktopRows.length - 1;
655
+ }
656
+ else if (event.key === "Tab") {
657
+ const next = row * visible.length + col + (event.shiftKey ? -1 : 1);
658
+ if (next < 0 || next >= desktopRows.length * visible.length)
659
+ return;
660
+ row = Math.floor(next / visible.length);
661
+ col = next % visible.length;
662
+ }
663
+ else
664
+ return;
665
+ event.preventDefault();
666
+ row = Math.max(0, Math.min(desktopRows.length - 1, row));
667
+ col = Math.max(0, Math.min(visible.length - 1, col));
668
+ const next = { rowId: getRowId(desktopRows[row]), fieldId: visible[col].field.id };
669
+ setAnchor(event.shiftKey && event.key !== "Tab" ? anchor ?? focus : null);
670
+ setFocus(next);
671
+ if (virtual) {
672
+ const index = flatEntries.findIndex((e) => e.kind === "row" && getRowId(e.row) === next.rowId);
673
+ if (index < virtualStart || index >= virtualEnd) {
674
+ if (scrollRef.current)
675
+ scrollRef.current.scrollTop = windowed.offsets[index];
676
+ setScrollTop(windowed.offsets[index]);
677
+ }
678
+ }
679
+ requestAnimationFrame(() => {
680
+ const cell = rootRef.current?.querySelector(`td[data-row-id="${CSS.escape(next.rowId)}"][data-field-id="${CSS.escape(next.fieldId)}"]`);
681
+ cell?.focus({ preventScroll: true });
682
+ cell?.scrollIntoView({ block: "nearest", inline: "nearest" });
683
+ });
684
+ }
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
+ const range = gridRange();
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"} 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
+ await navigator.clipboard.writeText(formatReadableRow(detailRow, availableFields));
695
+ setOperation({ state: "saved", message: "已複製可讀內容" });
696
+ }
697
+ catch {
698
+ reportFailure(new Error("無法使用剪貼簿,請確認瀏覽器權限"));
699
+ } }, children: "\u8907\u88FD\u6587\u5B57" }), props.renderDetailFooter?.(detailContext(detailRow)), operation.state !== "idle" && _jsx("p", { role: "status", children: operation.message })] }), dialog && _jsxs(DataViewDialog, { title: dialog === "add-field" ? "新增欄位" : dialog === "fields" ? "管理欄位" : dialog === "batch" ? "批次修改" : "確認資料修改", busy: busy, onClose: () => { setDialog(null); setPending(null); }, children: [dialog === "add-field" && _jsx(FieldCreator, { fields: fields, permissions: permissions, onApply: async (change) => { const saved = await applyFieldChange(change); if (saved)
700
+ setDialog(null); return saved; }, busy: busy, onCancel: () => setDialog(null) }), dialog === "fields" && _jsx(FieldManager, { fields: availableFields, permissions: permissions, onApply: applyFieldChange, busy: busy }), dialog === "batch" && _jsx(BatchEditor, { fields: availableFields, rows: selectedRows, getRowId: getRowId, permissions: permissions, onReview: reviewChanges }), dialog === "review" && pending && _jsxs("div", { className: "hy-dv-form", children: [_jsxs("p", { children: ["\u5171 ", pending.changes.length, " \u683C\u3002\u6240\u6709\u6B04\u4F4D\u9A57\u8B49\u901A\u904E\u5F8C\uFF0C\u7531\u5C08\u6848\u4E00\u6B21\u5BEB\u5165\uFF1B\u5931\u6557\u6216\u885D\u7A81\u6642\u4FDD\u7559\u539F\u8CC7\u6599\u3002"] }), _jsx("div", { className: "hy-dv-review-list", children: pending.changes.slice(0, 30).map((change) => _jsxs("div", { children: [_jsxs("strong", { children: [change.rowId, "\uFF0F", change.field.label] }), _jsxs("span", { children: [formatClipboardValue(change.previousValue, change.field) || "空白", " \u2192 ", formatClipboardValue(change.nextValue, change.field) || "空白"] })] }, `${change.rowId}:${change.field.id}`)) }), pending.changes.length > 30 && _jsxs("p", { children: ["\u9810\u89BD\u524D 30 \u683C\uFF0C\u5168\u90E8 ", pending.changes.length, " \u683C\u90FD\u6703\u9A57\u8B49\u3002"] }), _jsxs("button", { disabled: busy, onClick: async () => { if (await execute(pending)) {
701
+ setDialog(null);
702
+ setPending(null);
703
+ } }, children: ["\u78BA\u8A8D\u5957\u7528 ", pending.changes.length, " \u683C"] })] }), operation.state !== "idle" && _jsx("p", { role: operation.state === "failed" || operation.state === "conflict" ? "alert" : "status", children: operation.message })] })] });
704
+ function renderDesktopRow(row) {
705
+ const rowId = getRowId(row);
706
+ const index = desktopRowIndices.get(rowId) ?? 0;
707
+ const colors = colorMap.get(rowId);
708
+ return _jsxs("tr", { "data-move-row": rowId, "aria-rowindex": index + 2, className: `${selectedSet.has(rowId) ? "is-selected" : ""} ${move.sourceId === rowId ? "is-moving" : ""} ${move.target?.targetRowId === rowId ? `is-drop-${move.target.position}` : ""}`, style: { height: rowHeight }, children: [controlColumn && _jsx("td", { className: "hy-dv-selection is-pinned", style: { left: 0 }, children: _jsxs("div", { className: "hy-dv-row-controls", children: [props.rowMove && _jsx("button", { className: "hy-dv-row-drag", "aria-label": `移動 ${readableLabel(row)}`, disabled: move.pending || props.rowMove.canMove?.(row) === false, onPointerDown: (e) => move.start(rowId, e), onKeyDown: (e) => move.key(rowId, e), children: "\u283F" }), selectable && _jsx("input", { type: "checkbox", checked: selected.includes(rowId), onChange: () => toggleRow(rowId), "aria-label": `選取資料 ${readableLabel(row)}` }), features.rowOpen === true && _jsx("button", { "aria-label": `開啟 ${readableLabel(row)}`, onClick: () => openRow(row), children: "\u2197" })] }) }), visible.map(({ field }, col) => {
709
+ const left = pinnedOffsets.get(field.id);
710
+ const right = rightOffsets.get(field.id);
711
+ const inRange = range && index >= range.fromRow && index <= range.toRow && col >= range.fromCol && col <= range.toCol;
712
+ return _jsx("td", { "data-hy-cell": true, "data-row-id": rowId, "data-field-id": field.id, tabIndex: range && focus ? focus.rowId === rowId && focus.fieldId === field.id ? 0 : -1 : index === 0 && col === 0 ? 0 : -1, "aria-label": `${rowId} ${field.label}`, onFocus: () => setFocus({ rowId, fieldId: field.id }), onClick: (event) => { if (event.shiftKey) {
713
+ setAnchor(anchor ?? focus);
714
+ setFocus({ rowId, fieldId: field.id });
715
+ } }, className: `${left === undefined && right === undefined ? "" : "is-pinned"} ${inRange ? "is-range" : ""}`, style: { left, right, backgroundColor: colors?.get(field.id) ?? colors?.get("row") }, children: _jsx("div", { className: "hy-dv-cell-content", children: renderCell(row, field, "desktop") }) }, field.id);
716
+ }), canManageFields && _jsx("td", { className: "hy-dv-add-column is-pinned", style: { right: 0 } })] }, rowId);
717
+ }
718
+ function renderColumnHeader(field, column, gantt = false) {
719
+ const left = (gantt ? ganttLeftOffsets : pinnedOffsets).get(field.id);
720
+ const right = (gantt ? ganttRightOffsets : rightOffsets).get(field.id);
721
+ return _jsxs("th", { draggable: configurable, onDragStart: () => setDraggedColumn(field.id), onDragOver: (event) => event.preventDefault(), onDrop: () => dropColumn(field.id), className: left === undefined && right === undefined ? "" : "is-pinned", style: { left, right }, children: [_jsx("span", { children: field.label }), activeView.sorts.findIndex((rule) => rule.fieldId === field.id) >= 0 && _jsxs("small", { children: [activeView.sorts.findIndex((rule) => rule.fieldId === field.id) + 1, activeView.sorts.find((rule) => rule.fieldId === field.id)?.direction === "asc" ? "↑" : "↓"] }), configurable && _jsx("span", { className: "hy-dv-resize", onPointerDown: (event) => resizeColumn(field.id, column.width, event), "aria-hidden": true })] }, field.id);
722
+ }
723
+ function renderMobileCard(row) {
724
+ const rowId = getRowId(row);
725
+ return _jsxs("article", { "data-move-row": rowId, className: `hy-dv-card ${selected.includes(rowId) ? "is-selected" : ""}`, style: { backgroundColor: colorMap.get(rowId)?.get("row") }, children: [selectable && _jsxs("label", { className: "hy-dv-card-select", children: [_jsx("input", { type: "checkbox", checked: selected.includes(rowId), onChange: () => toggleRow(rowId) }), " \u9078\u53D6"] }), activeView.mobile?.summaryFieldId && _jsx("button", { className: "hy-dv-mobile-summary", onClick: () => openRow(row), children: readableLabel(row) }), props.renderMobileRow ? props.renderMobileRow(detailContext(row)) : _jsx("dl", { children: mobileFields.map((field) => canRead(row, field) && _jsxs(Fragment, { children: [_jsx("dt", { children: field.label }), _jsx("dd", { style: { backgroundColor: colorMap.get(rowId)?.get(field.id) }, children: renderCell(row, field, "mobile") })] }, field.id)) }), props.rowMove && _jsx("button", { className: "hy-dv-row-drag", "aria-label": `移動 ${readableLabel(row)}`, disabled: move.pending || props.rowMove.canMove?.(row) === false, onPointerDown: (e) => move.start(rowId, e), onKeyDown: (e) => move.key(rowId, e), children: "\u283F" }), _jsx("button", { className: "hy-dv-detail-button", onClick: () => openRow(row), children: "\u67E5\u770B\u8A73\u60C5" })] }, rowId);
726
+ }
727
+ }
728
+ function ToolbarButton({ children, active = false, onClick }) {
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 })] });
733
+ }
734
+ function PanelShell({ title, children, onClose }) {
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] });
736
+ }
737
+ function SortPanel({ fields, view, onChange, onClose }) {
738
+ function update(index, patch) {
739
+ onChange({ ...view, sorts: view.sorts.map((rule, ruleIndex) => ruleIndex === index ? { ...rule, ...patch } : rule) });
740
+ }
741
+ return _jsxs(PanelShell, { title: "\u591A\u6B04\u6392\u5E8F", onClose: onClose, children: [_jsx("p", { children: "\u7531\u4E0A\u5230\u4E0B\u5957\u7528\uFF0C\u7B2C\u4E00\u5217\u512A\u5148\u3002" }), _jsxs("div", { className: "hy-dv-rules", children: [view.sorts.map((rule, index) => _jsxs("div", { className: "hy-dv-rule", children: [_jsx("span", { children: index + 1 }), _jsx("select", { value: rule.fieldId, onChange: (event) => update(index, { fieldId: event.target.value }), children: fields.map((field) => _jsx("option", { value: field.id, disabled: view.sorts.some((item, i) => i !== index && item.fieldId === field.id), children: field.label }, field.id)) }), _jsxs("select", { value: rule.direction, onChange: (event) => update(index, { direction: event.target.value }), children: [_jsx("option", { value: "asc", children: "\u5C0F \u2192 \u5927" }), _jsx("option", { value: "desc", children: "\u5927 \u2192 \u5C0F" })] }), _jsx("button", { type: "button", onClick: () => onChange({ ...view, sorts: view.sorts.filter((_, i) => i !== index) }), children: "\u79FB\u9664" })] }, `${rule.fieldId}-${index}`)), _jsx("button", { type: "button", className: "hy-dv-add", disabled: view.sorts.length >= fields.length, onClick: () => {
742
+ const field = fields.find((item) => !view.sorts.some((rule) => rule.fieldId === item.id));
743
+ if (field)
744
+ onChange({ ...view, sorts: [...view.sorts, { fieldId: field.id, direction: "asc" }] });
745
+ }, children: "\uFF0B \u589E\u52A0\u6392\u5E8F" })] })] });
746
+ }
747
+ function FilterPanel({ fields, view, onChange, onClose }) {
748
+ function update(index, patch) {
749
+ onChange({ ...view, filters: { ...view.filters, rules: view.filters.rules.map((rule, i) => i === index ? { ...rule, ...patch } : rule) } });
750
+ }
751
+ return _jsxs(PanelShell, { title: "\u7BE9\u9078\u689D\u4EF6", onClose: onClose, children: [_jsxs("div", { className: "hy-dv-conjunction", children: [_jsx("span", { children: "\u689D\u4EF6\u95DC\u4FC2" }), _jsxs("select", { value: view.filters.conjunction, onChange: (event) => onChange({ ...view, filters: { ...view.filters, conjunction: event.target.value } }), children: [_jsx("option", { value: "and", children: "\u5168\u90E8\u7B26\u5408\uFF08AND\uFF09" }), _jsx("option", { value: "or", children: "\u4EFB\u4E00\u7B26\u5408\uFF08OR\uFF09" })] })] }), _jsxs("div", { className: "hy-dv-rules", children: [view.filters.rules.map((rule, index) => {
752
+ const field = fields.find((item) => item.id === rule.fieldId) ?? fields[0];
753
+ const operators = getFieldTypeDefinition(field.type).operators;
754
+ return _jsxs("div", { className: "hy-dv-rule hy-dv-filter-rule", children: [_jsx("select", { value: rule.fieldId, onChange: (event) => {
755
+ const nextField = fields.find((item) => item.id === event.target.value);
756
+ update(index, { fieldId: nextField.id, operator: getFieldTypeDefinition(nextField.type).operators[0], value: undefined });
757
+ }, children: fields.map((item) => _jsx("option", { value: item.id, children: item.label }, item.id)) }), _jsx("select", { value: rule.operator, onChange: (event) => update(index, { operator: event.target.value }), children: operators.map((operator) => _jsx("option", { value: operator, children: FILTER_OPERATOR_LABELS[operator] }, operator)) }), _jsx(FilterValueInput, { field: field, rule: rule, onChange: (value) => update(index, { value }) }), _jsx("button", { type: "button", onClick: () => onChange({ ...view, filters: { ...view.filters, rules: view.filters.rules.filter((_, i) => i !== index) } }), children: "\u79FB\u9664" })] }, rule.id);
758
+ }), _jsx("button", { type: "button", className: "hy-dv-add", disabled: fields.length === 0 || view.filters.rules.length >= 20, onClick: () => {
759
+ const field = fields[0];
760
+ if (!field)
761
+ return;
762
+ onChange({ ...view, filters: { ...view.filters, rules: [...view.filters.rules, { id: createRuleId(), fieldId: field.id, operator: getFieldTypeDefinition(field.type).operators[0] }] } });
763
+ }, children: "\uFF0B \u589E\u52A0\u689D\u4EF6" })] })] });
764
+ }
765
+ function FilterValueInput({ field, rule, onChange }) {
766
+ if (["isEmpty", "isNotEmpty", "isTrue", "isFalse"].includes(rule.operator))
767
+ return _jsx("span", { className: "hy-dv-no-value", children: "\u4E0D\u9700\u586B\u503C" });
768
+ if (field.options?.length)
769
+ return _jsxs("select", { value: String(rule.value ?? ""), onChange: (event) => onChange(event.target.value), children: [_jsx("option", { value: "", children: "\u8ACB\u9078\u64C7" }), field.options.map((option) => _jsx("option", { value: option.value, children: option.label }, option.value))] });
770
+ if (["number", "currency", "percentage", "autoNumber"].includes(field.type))
771
+ return _jsx("input", { type: "number", value: String(rule.value ?? ""), onChange: (event) => onChange(event.target.value === "" ? undefined : Number(event.target.value)) });
772
+ if (["date", "dateTime", "createdAt", "updatedAt"].includes(field.type))
773
+ return _jsx("input", { type: "date", value: String(rule.value ?? "").slice(0, 10), onChange: (event) => onChange(event.target.value) });
774
+ return _jsx("input", { value: String(rule.value ?? ""), onChange: (event) => onChange(event.target.value), placeholder: "\u8F38\u5165\u689D\u4EF6" });
775
+ }
776
+ function GroupPanel({ fields, view, onChange, onClose }) {
777
+ return _jsxs(PanelShell, { title: "\u5206\u7D44\u8A2D\u5B9A", onClose: onClose, children: [_jsx("p", { children: "\u6700\u591A\u4E09\u5C64\uFF1B\u7531\u4E0A\u5230\u4E0B\u5957\u7528\u3002" }), _jsxs("div", { className: "hy-dv-rules", children: [view.groups.map((rule, index) => _jsxs("div", { className: "hy-dv-rule", children: [_jsx("span", { children: index + 1 }), _jsx("select", { value: rule.fieldId, onChange: (event) => onChange({ ...view, groups: view.groups.map((item, i) => i === index ? { fieldId: event.target.value } : item) }), children: fields.map((field) => _jsx("option", { value: field.id, disabled: view.groups.some((item, i) => i !== index && item.fieldId === field.id), children: field.label }, field.id)) }), _jsx("button", { type: "button", onClick: () => onChange({ ...view, groups: view.groups.filter((_, i) => i !== index) }), children: "\u79FB\u9664" })] }, `${rule.fieldId}-${index}`)), _jsx("button", { type: "button", className: "hy-dv-add", disabled: view.groups.length >= Math.min(MAX_GROUP_LEVELS, fields.length), onClick: () => {
778
+ const field = fields.find((item) => !view.groups.some((rule) => rule.fieldId === item.id));
779
+ if (field)
780
+ onChange({ ...view, groups: [...view.groups, { fieldId: field.id }] });
781
+ }, children: "\uFF0B \u589E\u52A0\u5206\u7D44" })] })] });
782
+ }
783
+ function ColumnPanel({ fields, view, onChange, onClose, dragged, setDragged, drop }) {
784
+ const definitions = new Map(fields.map((field) => [field.id, field]));
785
+ const visibleCount = view.columns.filter((column) => column.visible).length;
786
+ function update(fieldId, patch) { onChange({ ...view, columns: view.columns.map((column) => column.fieldId === fieldId ? { ...column, ...patch } : column) }); }
787
+ return _jsxs(PanelShell, { title: "\u6B04\u4F4D\u986F\u793A\u3001\u9806\u5E8F\u8207\u5BEC\u5EA6", onClose: onClose, children: [_jsx("p", { children: "\u62D6\u66F3\u5217\u53EF\u8ABF\u6574\u9806\u5E8F\uFF1B\u5BEC\u5EA6\u9650\u5236 56\u2013600px\u3002" }), _jsx("div", { className: "hy-dv-column-list", children: view.columns.map((column, index) => _jsxs("div", { draggable: true, onDragStart: () => setDragged(column.fieldId), onDragEnd: () => setDragged(null), onDragOver: (event) => event.preventDefault(), onDrop: () => drop(column.fieldId), className: dragged === column.fieldId ? "is-dragging" : "", children: [_jsx("span", { className: "hy-dv-drag", "aria-hidden": true, children: "\u283F" }), _jsxs("label", { children: [_jsx("input", { type: "checkbox", checked: column.visible, disabled: column.visible && visibleCount === 1, onChange: (event) => update(column.fieldId, { visible: event.target.checked }) }), definitions.get(column.fieldId)?.label] }), _jsxs("select", { "aria-label": `${definitions.get(column.fieldId)?.label}固定位置`, value: column.pinned ?? "none", onChange: (event) => update(column.fieldId, { pinned: event.target.value === "none" ? null : event.target.value }), children: [_jsx("option", { value: "none", children: "\u4E0D\u56FA\u5B9A" }), _jsx("option", { value: "left", children: "\u56FA\u5B9A\u5DE6\u5074" }), _jsx("option", { value: "right", children: "\u56FA\u5B9A\u53F3\u5074" })] }), _jsx("input", { "aria-label": `${definitions.get(column.fieldId)?.label}欄寬`, type: "number", min: 56, max: 600, value: column.width, onChange: (event) => update(column.fieldId, { width: normalizeColumnWidth(Number(event.target.value)) }) }), _jsx("button", { "aria-label": `${definitions.get(column.fieldId)?.label}往前移`, disabled: index === 0, onClick: () => onChange({ ...view, columns: moveColumn(view.columns, column.fieldId, view.columns[index - 1].fieldId) }), children: "\u2191" }), _jsx("button", { "aria-label": `${definitions.get(column.fieldId)?.label}往後移`, disabled: index === view.columns.length - 1, onClick: () => { const next = [...view.columns]; [next[index], next[index + 1]] = [next[index + 1], next[index]]; onChange({ ...view, columns: next }); }, children: "\u2193" })] }, column.fieldId)) })] });
788
+ }
789
+ function SaveBadge({ state, prefix, message }) {
790
+ if (state === "idle")
791
+ return null;
792
+ const labels = { saving: "儲存中…", saved: "已儲存", failed: "儲存失敗", conflict: "版本衝突", readonly: "唯讀" };
793
+ return _jsxs("small", { className: `hy-dv-save is-${state}`, role: "status", title: message, children: [prefix ? `${prefix}:` : "", labels[state]] });
794
+ }
795
+ function StateCard({ title, message, tone = "normal" }) {
796
+ return _jsxs("div", { className: `hy-dv-state is-${tone}`, role: tone === "error" ? "alert" : "status", children: [_jsx("strong", { children: title }), _jsx("p", { children: message })] });
797
+ }
798
+ function LoadingState() {
799
+ return _jsxs("div", { className: "hy-dv-loading", "aria-label": "\u8F09\u5165\u4E2D", children: [_jsx("span", {}), _jsx("span", {}), _jsx("span", {}), _jsx("span", {})] });
800
+ }
801
+ function collectGroupKeys(nodes) {
802
+ return nodes.flatMap((node) => node.fieldId ? [node.key, ...collectGroupKeys(node.children)] : collectGroupKeys(node.children));
803
+ }
804
+ function createRuleId() {
805
+ return typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `filter-${Date.now()}-${Math.random().toString(36).slice(2)}`;
806
+ }
807
+ function flattenGroups(nodes, collapsed) {
808
+ const result = [];
809
+ for (const node of nodes) {
810
+ if (node.fieldId)
811
+ result.push({ kind: "group", node });
812
+ if (collapsed.has(node.key))
813
+ continue;
814
+ if (node.children.length)
815
+ result.push(...flattenGroups(node.children, collapsed));
816
+ else
817
+ for (const row of node.rows)
818
+ result.push({ kind: "row", row });
819
+ }
820
+ return result;
821
+ }
822
+ //# sourceMappingURL=data-view.js.map