mulmoclaude 2.6.0 → 2.8.0
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/client/assets/Graph-ChAxkn9o.js +1 -0
- package/client/assets/{JsonEditor-FKfzzMkt.js → JsonEditor-COQcmjLQ.js} +1 -1
- package/client/assets/dist-Bij44LdC.js +2 -0
- package/client/assets/echarts-GV3U9sVf.js +1 -0
- package/client/assets/echarts-gl-BXx4On0Y.js +4282 -0
- package/client/assets/echarts-wordcloud-_9uQUAfp.js +1 -0
- package/client/assets/emscripten-module-Q67P5WYC-BU3Hlttz.js +2160 -0
- package/client/assets/ffi-4V2b7xu3.js +1 -0
- package/client/assets/{html2canvas-D0m9Y8pD-y1tDAhR3.js → html2canvas-NBfz6Rb9-CIdEBoH2.js} +1 -1
- package/client/assets/{index-gnbD2OHU.js → index-D6xzCZhP.js} +605 -604
- package/client/assets/index-DzMsXDnj.css +2 -0
- package/client/assets/{index.es-HSMDtZ_a-CYtNwG_G.js → index.es-STZX6pYP-BGvt3MI2.js} +3 -3
- package/client/assets/install-C1moNcrn.js +15 -0
- package/client/assets/japan-CyDc-cz7-BgORDJsl.js +1 -0
- package/client/assets/{marp-BVojbGre.js → marp-Du5Sym5b.js} +57 -57
- package/client/assets/module-ES6BEMUI-CDTIpMiA.js +6 -0
- package/client/assets/purify.es-DkTnAgLw-BZxk_wWO.js +3 -0
- package/client/assets/style-BZwVK1dO-BFtilmZh.js +1 -0
- package/client/assets/vue-i18n-YREco4q1.js +3 -0
- package/client/assets/world-R-tMZ4nZ-B1c4T9X2.js +1 -0
- package/client/index.html +6 -4
- package/package.json +13 -12
- package/server/api/routes/plugins.ts +9 -59
- package/server/build/mcp-server.mjs +806 -714
- package/server/index.ts +1 -0
- package/server/plugins/confined-file-ops.ts +56 -0
- package/server/plugins/spreadsheet-builtin.ts +26 -0
- package/server/plugins/spreadsheet-save-outcome.ts +7 -0
- package/server/utils/files/store-resolvers.ts +1 -1
- package/src/lang/de.ts +0 -20
- package/src/lang/en.ts +0 -20
- package/src/lang/es.ts +0 -20
- package/src/lang/fr.ts +0 -20
- package/src/lang/ja.ts +0 -20
- package/src/lang/ko.ts +0 -20
- package/src/lang/pt-BR.ts +0 -20
- package/src/lang/zh.ts +0 -20
- package/src/plugins/spreadsheet/definition.ts +9 -127
- package/src/plugins/spreadsheet/index.ts +7 -4
- package/src/plugins/spreadsheet/meta.ts +2 -4
- package/client/assets/index-BA0fpWIZ.css +0 -2
- package/client/assets/purify.es-BaSjDiWD-Dbm5oOcm.js +0 -3
- package/client/assets/style-CCV5GAnU-D55tgtzm.js +0 -1
- package/client/assets/vue-i18n-2bgWpywp.js +0 -3
- package/server/utils/files/spreadsheet-store.ts +0 -26
- package/src/plugins/spreadsheet/Preview.vue +0 -31
- package/src/plugins/spreadsheet/View.vue +0 -1007
- package/src/plugins/spreadsheet/cellHighlights.ts +0 -78
- package/src/plugins/spreadsheet/engine/calculator.ts +0 -427
- package/src/plugins/spreadsheet/engine/cellBuilder.ts +0 -89
- package/src/plugins/spreadsheet/engine/cellEmpty.ts +0 -26
- package/src/plugins/spreadsheet/engine/cellFormatting.ts +0 -59
- package/src/plugins/spreadsheet/engine/coerce-boolean.ts +0 -26
- package/src/plugins/spreadsheet/engine/condition.ts +0 -182
- package/src/plugins/spreadsheet/engine/date-locale.ts +0 -47
- package/src/plugins/spreadsheet/engine/date-parser.ts +0 -234
- package/src/plugins/spreadsheet/engine/date-utils.ts +0 -73
- package/src/plugins/spreadsheet/engine/datedif.ts +0 -93
- package/src/plugins/spreadsheet/engine/engine.ts +0 -177
- package/src/plugins/spreadsheet/engine/evaluator.ts +0 -494
- package/src/plugins/spreadsheet/engine/financial-math.ts +0 -114
- package/src/plugins/spreadsheet/engine/formatter.ts +0 -158
- package/src/plugins/spreadsheet/engine/formulaError.ts +0 -67
- package/src/plugins/spreadsheet/engine/formulaRefs.ts +0 -202
- package/src/plugins/spreadsheet/engine/functions/date.ts +0 -212
- package/src/plugins/spreadsheet/engine/functions/financial.ts +0 -244
- package/src/plugins/spreadsheet/engine/functions/index.ts +0 -16
- package/src/plugins/spreadsheet/engine/functions/logical.ts +0 -221
- package/src/plugins/spreadsheet/engine/functions/lookup-math.ts +0 -28
- package/src/plugins/spreadsheet/engine/functions/lookup.ts +0 -257
- package/src/plugins/spreadsheet/engine/functions/mathematical.ts +0 -283
- package/src/plugins/spreadsheet/engine/functions/statistical-math.ts +0 -91
- package/src/plugins/spreadsheet/engine/functions/statistical.ts +0 -290
- package/src/plugins/spreadsheet/engine/functions/text.ts +0 -398
- package/src/plugins/spreadsheet/engine/index.ts +0 -34
- package/src/plugins/spreadsheet/engine/jsonCellLocator.ts +0 -111
- package/src/plugins/spreadsheet/engine/math-ops.ts +0 -98
- package/src/plugins/spreadsheet/engine/numericCoercion.ts +0 -61
- package/src/plugins/spreadsheet/engine/parser.ts +0 -40
- package/src/plugins/spreadsheet/engine/registry.ts +0 -201
- package/src/plugins/spreadsheet/engine/responseDecoder.ts +0 -68
- package/src/plugins/spreadsheet/engine/spreadsheet-errors.ts +0 -82
- package/src/plugins/spreadsheet/engine/textFormat.ts +0 -108
- package/src/plugins/spreadsheet/engine/translateFormula.ts +0 -69
- package/src/plugins/spreadsheet/engine/types.ts +0 -81
- package/src/plugins/spreadsheet/keyboardNav.ts +0 -38
|
@@ -1,1007 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div class="spreadsheet-container">
|
|
3
|
-
<div v-if="loading" class="min-h-full p-8 flex items-center justify-center">
|
|
4
|
-
<div class="text-gray-500">{{ t("pluginSpreadsheet.loading") }}</div>
|
|
5
|
-
</div>
|
|
6
|
-
<div v-else-if="errorMessage" class="min-h-full p-8 flex items-center justify-center">
|
|
7
|
-
<div class="error">{{ errorMessage }}</div>
|
|
8
|
-
</div>
|
|
9
|
-
<div v-else-if="!resolvedSheets || resolvedSheets.length === 0" class="min-h-full p-8 flex items-center justify-center">
|
|
10
|
-
<div class="text-gray-500">{{ t("pluginSpreadsheet.noData") }}</div>
|
|
11
|
-
</div>
|
|
12
|
-
<template v-else>
|
|
13
|
-
<div class="spreadsheet-content-wrapper">
|
|
14
|
-
<div class="p-4">
|
|
15
|
-
<div class="header">
|
|
16
|
-
<h1 class="title">
|
|
17
|
-
{{ selectedResult.title || t("pluginSpreadsheet.untitled") }}
|
|
18
|
-
</h1>
|
|
19
|
-
<!-- `download-btn` class kept as a name hook — referenced by
|
|
20
|
-
e2e/tests/spreadsheet.spec.ts via `button.download-btn`.
|
|
21
|
-
Styling is inline Tailwind; the class has no CSS rules. -->
|
|
22
|
-
<button
|
|
23
|
-
class="download-btn h-8 px-2.5 flex items-center gap-1 rounded bg-[#217346] hover:bg-[#1e6a3f] active:bg-[#1a5c36] text-white text-sm transition-colors"
|
|
24
|
-
@click="downloadExcel"
|
|
25
|
-
>
|
|
26
|
-
<span class="material-icons text-base">download</span>
|
|
27
|
-
{{ t("pluginSpreadsheet.excel") }}
|
|
28
|
-
</button>
|
|
29
|
-
</div>
|
|
30
|
-
|
|
31
|
-
<!-- Sheet tabs (if multiple sheets) -->
|
|
32
|
-
<div v-if="resolvedSheets.length > 1" class="sheet-tabs">
|
|
33
|
-
<button
|
|
34
|
-
v-for="(sheet, index) in resolvedSheets"
|
|
35
|
-
:key="index"
|
|
36
|
-
:class="['sheet-tab', { active: activeSheetIndex === index }]"
|
|
37
|
-
@click="activeSheetIndex = index"
|
|
38
|
-
>
|
|
39
|
-
{{ sheet.name }}
|
|
40
|
-
</button>
|
|
41
|
-
</div>
|
|
42
|
-
|
|
43
|
-
<!-- Spreadsheet table -->
|
|
44
|
-
<!-- eslint-disable-next-line vue/no-v-html -- XLSX.utils.sheet_to_html output of app-owned sheet data; trusted in-process render -->
|
|
45
|
-
<div ref="tableContainer" class="table-container" @click="handleTableClick" v-html="renderedHtml"></div>
|
|
46
|
-
</div>
|
|
47
|
-
</div>
|
|
48
|
-
|
|
49
|
-
<!-- Collapsible Editor -->
|
|
50
|
-
<details v-if="!miniEditorOpen" ref="editorDetails" class="spreadsheet-source">
|
|
51
|
-
<summary>{{ t("pluginSpreadsheet.editData") }}</summary>
|
|
52
|
-
<textarea ref="editorTextarea" v-model="editableData" class="spreadsheet-editor" spellcheck="false" @input="handleDataEdit"></textarea>
|
|
53
|
-
<button class="apply-btn" :disabled="!hasChanges" @click="applyChanges">{{ t("pluginSpreadsheet.applyChanges") }}</button>
|
|
54
|
-
</details>
|
|
55
|
-
|
|
56
|
-
<!-- Mini Editor at Bottom -->
|
|
57
|
-
<div v-if="miniEditorOpen" class="mini-editor-panel">
|
|
58
|
-
<div class="mini-editor-content">
|
|
59
|
-
<span v-if="miniEditorCell" class="cell-ref"> {{ indexToCol(miniEditorCell.col) }}{{ miniEditorCell.row + 1 }} </span>
|
|
60
|
-
|
|
61
|
-
<!-- Type Selector -->
|
|
62
|
-
<div class="radio-group">
|
|
63
|
-
<label class="radio-option">
|
|
64
|
-
<input v-model="miniEditorType" type="radio" value="string" />
|
|
65
|
-
{{ t("pluginSpreadsheet.stringType") }}
|
|
66
|
-
</label>
|
|
67
|
-
<label class="radio-option">
|
|
68
|
-
<input v-model="miniEditorType" type="radio" value="object" />
|
|
69
|
-
{{ t("pluginSpreadsheet.formulaType") }}
|
|
70
|
-
</label>
|
|
71
|
-
</div>
|
|
72
|
-
|
|
73
|
-
<!-- String input -->
|
|
74
|
-
<input
|
|
75
|
-
v-if="miniEditorType === 'string'"
|
|
76
|
-
v-model="miniEditorValue"
|
|
77
|
-
type="text"
|
|
78
|
-
class="form-input"
|
|
79
|
-
:placeholder="t('pluginSpreadsheet.valuePlaceholder')"
|
|
80
|
-
@keyup.enter="saveMiniEditor"
|
|
81
|
-
/>
|
|
82
|
-
|
|
83
|
-
<!-- Formula inputs -->
|
|
84
|
-
<template v-if="miniEditorType === 'object'">
|
|
85
|
-
<input
|
|
86
|
-
v-model="miniEditorFormula"
|
|
87
|
-
type="text"
|
|
88
|
-
class="form-input"
|
|
89
|
-
:placeholder="t('pluginSpreadsheet.valueOrFormulaPlaceholder')"
|
|
90
|
-
@keyup.enter="saveMiniEditor"
|
|
91
|
-
/>
|
|
92
|
-
<input
|
|
93
|
-
v-model="miniEditorFormat"
|
|
94
|
-
type="text"
|
|
95
|
-
class="form-input"
|
|
96
|
-
:placeholder="t('pluginSpreadsheet.formatPlaceholder')"
|
|
97
|
-
@keyup.enter="saveMiniEditor"
|
|
98
|
-
/>
|
|
99
|
-
</template>
|
|
100
|
-
|
|
101
|
-
<button class="save-btn" @click="saveMiniEditor">{{ t("pluginSpreadsheet.update") }}</button>
|
|
102
|
-
<button class="cancel-btn" @click="closeMiniEditor">✕</button>
|
|
103
|
-
</div>
|
|
104
|
-
</div>
|
|
105
|
-
</template>
|
|
106
|
-
</div>
|
|
107
|
-
</template>
|
|
108
|
-
|
|
109
|
-
<script setup lang="ts">
|
|
110
|
-
import { computed, ref, watch, onMounted, onUnmounted } from "vue";
|
|
111
|
-
import { useI18n } from "vue-i18n";
|
|
112
|
-
import * as XLSX from "xlsx";
|
|
113
|
-
import type { ToolResult } from "gui-chat-protocol";
|
|
114
|
-
import type { SpreadsheetToolData, SpreadsheetSheet, SpreadsheetCellValue, StorableCell, SpreadsheetEndpoints } from "./definition";
|
|
115
|
-
import {
|
|
116
|
-
SpreadsheetEngine,
|
|
117
|
-
prefersDayFirst,
|
|
118
|
-
indexToColumn,
|
|
119
|
-
extractCellReferences,
|
|
120
|
-
buildCellFromInput,
|
|
121
|
-
normalizeData,
|
|
122
|
-
decodeSpreadsheetResponse,
|
|
123
|
-
findCellJsonPosition,
|
|
124
|
-
type SpreadsheetCell,
|
|
125
|
-
type SheetData,
|
|
126
|
-
type CellValue,
|
|
127
|
-
} from "./engine";
|
|
128
|
-
import { applyCellHighlights, clearCellHighlights } from "./cellHighlights";
|
|
129
|
-
import { getArrowKeyOffset, isWithinSheetBounds } from "./keyboardNav";
|
|
130
|
-
import { handleExternalLinkClick } from "@mulmoclaude/markdown-utils/dom/externalLink";
|
|
131
|
-
import { errorMessage as formatErrorMessage } from "../../utils/errors";
|
|
132
|
-
import { escapeHtml } from "@mulmoclaude/core/wiki";
|
|
133
|
-
|
|
134
|
-
// Import all spreadsheet functions to populate the function registry
|
|
135
|
-
import "./engine/functions";
|
|
136
|
-
import { apiGet, apiPut } from "../../utils/api";
|
|
137
|
-
import { pluginEndpoints } from "../api";
|
|
138
|
-
import type { FilesContentResponseLike } from "./engine/responseDecoder";
|
|
139
|
-
import { isObj, isRecord, isUnknownArray } from "../../utils/types";
|
|
140
|
-
|
|
141
|
-
const endpoints = pluginEndpoints<SpreadsheetEndpoints>("spreadsheet");
|
|
142
|
-
const filesEndpoints = pluginEndpoints<{ content: string }>("files");
|
|
143
|
-
|
|
144
|
-
const { t, locale } = useI18n();
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Normalize malformed data structures
|
|
148
|
-
* Some models generate flat arrays instead of 2D arrays - fix them
|
|
149
|
-
*/
|
|
150
|
-
|
|
151
|
-
// Cells can be raw LLM output of arbitrary shape; tightening here would cascade through the engine API.
|
|
152
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
153
|
-
function normalizeSheetData(data: any): any[][] {
|
|
154
|
-
// Handle null/undefined
|
|
155
|
-
if (!data) {
|
|
156
|
-
return [];
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// If not an array
|
|
160
|
-
if (!Array.isArray(data)) {
|
|
161
|
-
return [];
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Empty array
|
|
165
|
-
if (data.length === 0) {
|
|
166
|
-
return [];
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// If data is already a 2D array, return as-is
|
|
170
|
-
if (Array.isArray(data[0])) {
|
|
171
|
-
return data;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// If data is a flat array of cell objects, convert to 2D by pairing cells
|
|
175
|
-
// Pattern: [cell1, cell2, cell3, cell4] -> [[cell1, cell2], [cell3, cell4]]
|
|
176
|
-
if (isObj(data[0])) {
|
|
177
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
178
|
-
const rows: any[][] = [];
|
|
179
|
-
for (let i = 0; i < data.length; i += 2) {
|
|
180
|
-
const row = [data[i]];
|
|
181
|
-
if (i + 1 < data.length) {
|
|
182
|
-
row.push(data[i + 1]);
|
|
183
|
-
}
|
|
184
|
-
rows.push(row);
|
|
185
|
-
}
|
|
186
|
-
return rows;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// Unknown structure - return empty
|
|
190
|
-
return [];
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const props = defineProps<{
|
|
194
|
-
selectedResult: ToolResult<SpreadsheetToolData>;
|
|
195
|
-
}>();
|
|
196
|
-
|
|
197
|
-
const emit = defineEmits<{
|
|
198
|
-
updateResult: [result: ToolResult];
|
|
199
|
-
}>();
|
|
200
|
-
|
|
201
|
-
// Create spreadsheet engine instance
|
|
202
|
-
const engine = new SpreadsheetEngine();
|
|
203
|
-
|
|
204
|
-
const loading = ref(false);
|
|
205
|
-
const errorMessage = ref("");
|
|
206
|
-
const resolvedSheets = ref<SpreadsheetSheet[]>([]);
|
|
207
|
-
|
|
208
|
-
// Accepts only the canonical prefix. Legacy `spreadsheets/*.json`
|
|
209
|
-
// references in old session JSONL are no longer auto-resolved —
|
|
210
|
-
// those sessions render the file path as plain text.
|
|
211
|
-
function isFilePath(value: unknown): value is string {
|
|
212
|
-
return typeof value === "string" && value.startsWith("artifacts/spreadsheets/") && value.endsWith(".json");
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
const isCellValue = (value: unknown): value is SpreadsheetCellValue => typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
216
|
-
|
|
217
|
-
// Each predicate claims exactly the union member it proves: `StorableCell`
|
|
218
|
-
// covers the four forms a stored workbook holds — `{ v, f }`, an empty `{}` or
|
|
219
|
-
// `{ v: null }`, a bare value, and a `null` gap — because `normalizeData`
|
|
220
|
-
// folds all four and a reader that took only `{ v }` would reject workbooks
|
|
221
|
-
// the app renders today.
|
|
222
|
-
const isStorableCell = (value: unknown): value is StorableCell =>
|
|
223
|
-
value === null ||
|
|
224
|
-
isCellValue(value) ||
|
|
225
|
-
(isRecord(value) && (value.v === undefined || value.v === null || isCellValue(value.v)) && (value.f === undefined || typeof value.f === "string"));
|
|
226
|
-
|
|
227
|
-
const isCellRow = (value: unknown): value is StorableCell[] => isUnknownArray(value) && value.every(isStorableCell);
|
|
228
|
-
|
|
229
|
-
const isSheet = (value: unknown): value is SpreadsheetSheet =>
|
|
230
|
-
isRecord(value) && typeof value.name === "string" && isUnknownArray(value.data) && value.data.every(isCellRow);
|
|
231
|
-
|
|
232
|
-
/** Sheets replayed from session JSONL, where the declared type is a claim
|
|
233
|
-
* about whatever wrote the log rather than about this value. Taken whole or
|
|
234
|
-
* rejected whole — dropping the unreadable half of a workbook would hide
|
|
235
|
-
* part of a user's file behind cells that merely look empty. */
|
|
236
|
-
function readReplayedSheets(value: unknown): SpreadsheetSheet[] | null {
|
|
237
|
-
return isUnknownArray(value) && value.every(isSheet) ? value : null;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// Editor textarea backing ref. Declared up here (rather than next to
|
|
241
|
-
// the other view-state refs further down) so the `fetchSheets().then`
|
|
242
|
-
// initializer below can assign to it without TDZ.
|
|
243
|
-
const editableData = ref(JSON.stringify(resolvedSheets.value || [], null, 2));
|
|
244
|
-
|
|
245
|
-
async function fetchSheets(): Promise<void> {
|
|
246
|
-
const raw = props.selectedResult.data?.sheets;
|
|
247
|
-
// Clear any stale error from a previous result BEFORE the early
|
|
248
|
-
// returns, otherwise switching from a failed file-backed load to
|
|
249
|
-
// a new inline-data result leaves the error panel on screen.
|
|
250
|
-
errorMessage.value = "";
|
|
251
|
-
if (!raw) {
|
|
252
|
-
resolvedSheets.value = [];
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
255
|
-
if (!isFilePath(raw)) {
|
|
256
|
-
// Legacy inline data
|
|
257
|
-
const inline = readReplayedSheets(raw);
|
|
258
|
-
resolvedSheets.value = inline ?? [];
|
|
259
|
-
if (!inline) errorMessage.value = t("pluginSpreadsheet.dataMustBeArray");
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
loading.value = true;
|
|
263
|
-
const response = await apiGet<FilesContentResponseLike>(filesEndpoints.content, { path: raw });
|
|
264
|
-
if (!response.ok) {
|
|
265
|
-
errorMessage.value = t("pluginSpreadsheet.loadFailed", { error: response.error });
|
|
266
|
-
resolvedSheets.value = [];
|
|
267
|
-
loading.value = false;
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
270
|
-
// The /files/content endpoint returns { kind, content?, message? }.
|
|
271
|
-
// Delegate the shape/validation decision to decodeSpreadsheetResponse
|
|
272
|
-
// so the async wrapper stays simple.
|
|
273
|
-
const result = decodeSpreadsheetResponse(response.data);
|
|
274
|
-
if (result.kind === "error") {
|
|
275
|
-
errorMessage.value = result.message;
|
|
276
|
-
resolvedSheets.value = [];
|
|
277
|
-
} else {
|
|
278
|
-
resolvedSheets.value = result.sheets;
|
|
279
|
-
}
|
|
280
|
-
loading.value = false;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// Fetch on mount and sync editableData
|
|
284
|
-
fetchSheets().then(() => {
|
|
285
|
-
editableData.value = JSON.stringify(resolvedSheets.value || [], null, 2);
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
/** Persist edited sheets to disk when file-backed, and emit updateResult. */
|
|
289
|
-
async function persistSheets(sheets: SpreadsheetSheet[]): Promise<void> {
|
|
290
|
-
const raw = props.selectedResult.data?.sheets;
|
|
291
|
-
if (isFilePath(raw)) {
|
|
292
|
-
// Send the full workspace-relative path so the route doesn't have
|
|
293
|
-
// to reconstruct one from a basename — paths under
|
|
294
|
-
// `artifacts/spreadsheets/` are now sharded by YYYY/MM (#764).
|
|
295
|
-
const result = await apiPut<unknown>(endpoints.update.url, {
|
|
296
|
-
relativePath: raw,
|
|
297
|
-
sheets,
|
|
298
|
-
});
|
|
299
|
-
if (!result.ok) {
|
|
300
|
-
errorMessage.value = `Failed to save spreadsheet: ${result.error}`;
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
resolvedSheets.value = sheets;
|
|
306
|
-
|
|
307
|
-
const updatedResult: ToolResult<SpreadsheetToolData> = {
|
|
308
|
-
...props.selectedResult,
|
|
309
|
-
data: {
|
|
310
|
-
...props.selectedResult.data,
|
|
311
|
-
sheets: isFilePath(raw) ? raw : sheets,
|
|
312
|
-
},
|
|
313
|
-
};
|
|
314
|
-
emit("updateResult", updatedResult);
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
const activeSheetIndex = ref(0);
|
|
318
|
-
// Editor state declared together with the other refs above. Seeded
|
|
319
|
-
// in the on-mount `fetchSheets().then(...)` block above (the `.value`
|
|
320
|
-
// assignment is in scope by the time that .then callback fires).
|
|
321
|
-
const editorTextarea = ref<HTMLTextAreaElement | null>(null);
|
|
322
|
-
const editorDetails = ref<HTMLDetailsElement | null>(null);
|
|
323
|
-
const tableContainer = ref<HTMLDivElement | null>(null);
|
|
324
|
-
|
|
325
|
-
// Mini editor state
|
|
326
|
-
const miniEditorOpen = ref(false);
|
|
327
|
-
const miniEditorCell = ref<{ row: number; col: number } | null>(null);
|
|
328
|
-
|
|
329
|
-
const miniEditorValue = ref<unknown>(null);
|
|
330
|
-
const miniEditorType = ref<"number" | "string" | "object">("string");
|
|
331
|
-
const miniEditorFormula = ref("");
|
|
332
|
-
const miniEditorFormat = ref("");
|
|
333
|
-
|
|
334
|
-
// Referenced cells state (for formula highlighting)
|
|
335
|
-
const referencedCells = ref<{ row: number; col: number }[]>([]);
|
|
336
|
-
|
|
337
|
-
// Check if spreadsheet data has been modified
|
|
338
|
-
const hasChanges = computed(() => {
|
|
339
|
-
try {
|
|
340
|
-
const currentData = JSON.stringify(resolvedSheets.value || [], null, 2);
|
|
341
|
-
return editableData.value !== currentData;
|
|
342
|
-
} catch {
|
|
343
|
-
return false;
|
|
344
|
-
}
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
// Short alias used in the template column header.
|
|
348
|
-
const indexToCol = indexToColumn;
|
|
349
|
-
|
|
350
|
-
// `normalizeData` is the engine's own reader for the stored cell forms (bare
|
|
351
|
-
// values, `null` gaps, flat rows); `calculate` runs it internally anyway, so
|
|
352
|
-
// applying it here only meets the engine's declared type at the call site.
|
|
353
|
-
const toEngineSheet = (sheet: SpreadsheetSheet): SheetData => ({ name: sheet.name, data: normalizeData(sheet.data) });
|
|
354
|
-
|
|
355
|
-
// Calculate formulas in the data using the spreadsheet engine
|
|
356
|
-
const calculateFormulas = (data: StorableCell[][], sheetName?: string): CellValue[][] => {
|
|
357
|
-
// If we have a sheet name, we need to find all sheets for cross-sheet references
|
|
358
|
-
const allSheets = resolvedSheets.value.map(toEngineSheet);
|
|
359
|
-
|
|
360
|
-
// Create a SheetData object for the engine
|
|
361
|
-
const sheet: SheetData = {
|
|
362
|
-
name: sheetName || "Sheet1",
|
|
363
|
-
data: normalizeData(data),
|
|
364
|
-
};
|
|
365
|
-
|
|
366
|
-
// Read the date order from the current locale on every calculation, so a
|
|
367
|
-
// locale switch takes effect without rebuilding the engine.
|
|
368
|
-
engine.setOptions({ preferDDMMYYYY: prefersDayFirst(locale.value) });
|
|
369
|
-
const result = engine.calculate(sheet, allSheets);
|
|
370
|
-
|
|
371
|
-
// Return the calculated data
|
|
372
|
-
|
|
373
|
-
return result.data;
|
|
374
|
-
};
|
|
375
|
-
|
|
376
|
-
// Render the active sheet as HTML table
|
|
377
|
-
const renderedHtml = computed(() => {
|
|
378
|
-
if (!resolvedSheets.value || resolvedSheets.value.length === 0) {
|
|
379
|
-
return "";
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
const sheet = resolvedSheets.value[activeSheetIndex.value];
|
|
383
|
-
if (!sheet || !sheet.data) {
|
|
384
|
-
return "";
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
try {
|
|
388
|
-
// Calculate formulas first with sheet name for cross-sheet references
|
|
389
|
-
const calculatedData = calculateFormulas(sheet.data, sheet.name);
|
|
390
|
-
|
|
391
|
-
// Convert data array to worksheet
|
|
392
|
-
const worksheet = XLSX.utils.aoa_to_sheet(calculatedData);
|
|
393
|
-
|
|
394
|
-
// Generate HTML table
|
|
395
|
-
const html = XLSX.utils.sheet_to_html(worksheet, {
|
|
396
|
-
id: "spreadsheet-table",
|
|
397
|
-
editable: false,
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
return html;
|
|
401
|
-
} catch (error) {
|
|
402
|
-
console.error("Failed to render spreadsheet:", error);
|
|
403
|
-
// Result is fed into v-html (see :data-testid="table"). The
|
|
404
|
-
// message comes from a caught throw, which can include attacker-
|
|
405
|
-
// controlled object shapes (e.g. `{ details: "<script>..." }`)
|
|
406
|
-
// surfaced by formatErrorMessage's gRPC-style unwrap. Escape
|
|
407
|
-
// before interpolation. (Codex review on #1767.)
|
|
408
|
-
return `<div class="error">Failed to render spreadsheet: ${escapeHtml(formatErrorMessage(error, "Unknown error"))}</div>`;
|
|
409
|
-
}
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
// Download as Excel file
|
|
413
|
-
const downloadExcel = () => {
|
|
414
|
-
if (!resolvedSheets.value || resolvedSheets.value.length === 0) return;
|
|
415
|
-
|
|
416
|
-
try {
|
|
417
|
-
const workbook = XLSX.utils.book_new();
|
|
418
|
-
|
|
419
|
-
// Add all sheets to workbook
|
|
420
|
-
resolvedSheets.value.forEach((sheet) => {
|
|
421
|
-
const worksheet = XLSX.utils.aoa_to_sheet(sheet.data);
|
|
422
|
-
XLSX.utils.book_append_sheet(workbook, worksheet, sheet.name);
|
|
423
|
-
});
|
|
424
|
-
|
|
425
|
-
// Generate filename
|
|
426
|
-
const filename = props.selectedResult.title ? `${props.selectedResult.title.replace(/[^a-z0-9]/gi, "_").toLowerCase()}.xlsx` : "spreadsheet.xlsx";
|
|
427
|
-
|
|
428
|
-
// Write file
|
|
429
|
-
XLSX.writeFile(workbook, filename);
|
|
430
|
-
} catch (error) {
|
|
431
|
-
console.error("Failed to download Excel:", error);
|
|
432
|
-
alert(`Failed to download Excel file: ${formatErrorMessage(error, "Unknown error")}`);
|
|
433
|
-
}
|
|
434
|
-
};
|
|
435
|
-
|
|
436
|
-
function handleDataEdit() {
|
|
437
|
-
// Just update the local state, don't apply yet
|
|
438
|
-
// User needs to click "Apply Changes" button
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
// `extractCellReferences` now lives in `./engine/formulaRefs.ts`
|
|
442
|
-
// (imported at the top of this file). Extracted to bring this
|
|
443
|
-
// file's cognitive complexity back under the sonarjs threshold
|
|
444
|
-
// and to make the formula-reference scanner unit-testable.
|
|
445
|
-
// See `test/plugins/spreadsheet/engine/test_formulaRefs.ts`.
|
|
446
|
-
|
|
447
|
-
function openMiniEditor(rowIndex: number, colIndex: number) {
|
|
448
|
-
try {
|
|
449
|
-
const sheets = JSON.parse(editableData.value);
|
|
450
|
-
const currentSheet = sheets[activeSheetIndex.value];
|
|
451
|
-
|
|
452
|
-
if (!currentSheet || !currentSheet.data) {
|
|
453
|
-
return;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
// Normalize the data in case it's malformed
|
|
457
|
-
const normalizedData = normalizeSheetData(currentSheet.data);
|
|
458
|
-
|
|
459
|
-
if (!normalizedData[rowIndex] || normalizedData[rowIndex][colIndex] === undefined) {
|
|
460
|
-
return;
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
const cellValue = normalizedData[rowIndex][colIndex];
|
|
464
|
-
|
|
465
|
-
// Determine cell type and extract values (new format: {v, f})
|
|
466
|
-
if (isRecord(cellValue) && "v" in cellValue) {
|
|
467
|
-
const value = cellValue.v;
|
|
468
|
-
const format = typeof cellValue.f === "string" ? cellValue.f : "";
|
|
469
|
-
|
|
470
|
-
// Check if it's a formula (value starts with "=")
|
|
471
|
-
if (typeof value === "string" && value.startsWith("=")) {
|
|
472
|
-
miniEditorType.value = "object";
|
|
473
|
-
miniEditorValue.value = "";
|
|
474
|
-
miniEditorFormula.value = value.substring(1); // Remove "=" prefix
|
|
475
|
-
miniEditorFormat.value = format;
|
|
476
|
-
// Extract and store referenced cells for highlighting
|
|
477
|
-
referencedCells.value = extractCellReferences(value);
|
|
478
|
-
} else if (typeof value === "number") {
|
|
479
|
-
miniEditorType.value = "object";
|
|
480
|
-
miniEditorValue.value = "";
|
|
481
|
-
miniEditorFormula.value = String(value);
|
|
482
|
-
miniEditorFormat.value = format;
|
|
483
|
-
referencedCells.value = [];
|
|
484
|
-
} else {
|
|
485
|
-
miniEditorType.value = "string";
|
|
486
|
-
miniEditorValue.value = String(value);
|
|
487
|
-
miniEditorFormula.value = "";
|
|
488
|
-
miniEditorFormat.value = "";
|
|
489
|
-
referencedCells.value = [];
|
|
490
|
-
}
|
|
491
|
-
} else {
|
|
492
|
-
// Legacy format or plain value
|
|
493
|
-
miniEditorType.value = "string";
|
|
494
|
-
miniEditorValue.value = String(cellValue ?? "");
|
|
495
|
-
miniEditorFormula.value = "";
|
|
496
|
-
miniEditorFormat.value = "";
|
|
497
|
-
referencedCells.value = [];
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
miniEditorCell.value = { row: rowIndex, col: colIndex };
|
|
501
|
-
miniEditorOpen.value = true;
|
|
502
|
-
} catch (error) {
|
|
503
|
-
console.error("Failed to open mini editor:", error);
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
function closeMiniEditor() {
|
|
508
|
-
miniEditorOpen.value = false;
|
|
509
|
-
miniEditorCell.value = null;
|
|
510
|
-
miniEditorValue.value = null;
|
|
511
|
-
miniEditorFormula.value = "";
|
|
512
|
-
miniEditorFormat.value = "";
|
|
513
|
-
referencedCells.value = [];
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
function saveMiniEditor() {
|
|
517
|
-
if (!miniEditorCell.value) return;
|
|
518
|
-
|
|
519
|
-
try {
|
|
520
|
-
const sheets = JSON.parse(editableData.value);
|
|
521
|
-
const currentSheet = sheets[activeSheetIndex.value];
|
|
522
|
-
|
|
523
|
-
if (!currentSheet || !currentSheet.data) return;
|
|
524
|
-
|
|
525
|
-
const { row, col } = miniEditorCell.value;
|
|
526
|
-
|
|
527
|
-
// Normalize the data in case it's malformed
|
|
528
|
-
const normalizedData = normalizeSheetData(currentSheet.data);
|
|
529
|
-
|
|
530
|
-
// Ensure the row exists
|
|
531
|
-
while (normalizedData.length <= row) {
|
|
532
|
-
normalizedData.push([]);
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
// Ensure the row is an array
|
|
536
|
-
if (!Array.isArray(normalizedData[row])) {
|
|
537
|
-
normalizedData[row] = [];
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
// Build the new cell value (delegates formula/number detection
|
|
541
|
-
// and format attachment to the pure helper).
|
|
542
|
-
const newCellValue: SpreadsheetCell = buildCellFromInput({
|
|
543
|
-
type: miniEditorType.value,
|
|
544
|
-
value: miniEditorValue.value,
|
|
545
|
-
formula: miniEditorFormula.value,
|
|
546
|
-
format: miniEditorFormat.value,
|
|
547
|
-
});
|
|
548
|
-
|
|
549
|
-
// Update the cell in normalized data
|
|
550
|
-
normalizedData[row][col] = newCellValue;
|
|
551
|
-
|
|
552
|
-
// Update the sheet with normalized data
|
|
553
|
-
currentSheet.data = normalizedData;
|
|
554
|
-
|
|
555
|
-
// Update editableData
|
|
556
|
-
editableData.value = JSON.stringify(sheets, null, 2);
|
|
557
|
-
|
|
558
|
-
// Persist to disk (if file-backed) and emit update
|
|
559
|
-
persistSheets(sheets);
|
|
560
|
-
|
|
561
|
-
// Update referenced cells if the saved cell contains a formula
|
|
562
|
-
if (typeof newCellValue.v === "string" && newCellValue.v.startsWith("=")) {
|
|
563
|
-
referencedCells.value = extractCellReferences(newCellValue.v);
|
|
564
|
-
} else {
|
|
565
|
-
referencedCells.value = [];
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
// Don't close the mini editor - keep it open so user can see the updated references
|
|
569
|
-
// closeMiniEditor();
|
|
570
|
-
} catch (error) {
|
|
571
|
-
alert(`Failed to save cell: ${formatErrorMessage(error, "Unknown error")}`);
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
function handleTableClick(event: MouseEvent) {
|
|
576
|
-
// External http(s) links inside cells (XLSX hyperlink fields) —
|
|
577
|
-
// open in a new tab instead of navigating the SPA away (#1221).
|
|
578
|
-
if (handleExternalLinkClick(event)) return;
|
|
579
|
-
const cell = event.target;
|
|
580
|
-
// HTMLTableCellElement covers <th> too, and only data cells carry coordinates.
|
|
581
|
-
if (!(cell instanceof HTMLTableCellElement) || cell.tagName !== "TD") return;
|
|
582
|
-
const row = cell.parentElement;
|
|
583
|
-
if (!(row instanceof HTMLTableRowElement)) return;
|
|
584
|
-
|
|
585
|
-
const colIndex = cell.cellIndex;
|
|
586
|
-
const { rowIndex } = row;
|
|
587
|
-
|
|
588
|
-
// Check if the main editor details is open
|
|
589
|
-
const isEditorOpen = editorDetails.value?.open ?? false;
|
|
590
|
-
|
|
591
|
-
// If editor is closed, open mini editor
|
|
592
|
-
if (!isEditorOpen) {
|
|
593
|
-
openMiniEditor(rowIndex, colIndex);
|
|
594
|
-
return;
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
// If editor is open, try to find and select this cell in the editor.
|
|
598
|
-
if (!editorTextarea.value) return;
|
|
599
|
-
try {
|
|
600
|
-
const sheets = JSON.parse(editableData.value);
|
|
601
|
-
const currentSheet = sheets[activeSheetIndex.value];
|
|
602
|
-
if (!currentSheet || !currentSheet.data) return;
|
|
603
|
-
const normalizedData = normalizeSheetData(currentSheet.data);
|
|
604
|
-
if (!normalizedData[rowIndex] || normalizedData[rowIndex][colIndex] === undefined) {
|
|
605
|
-
return;
|
|
606
|
-
}
|
|
607
|
-
const cellStr = JSON.stringify(normalizedData[rowIndex][colIndex]);
|
|
608
|
-
const cellStart = findCellJsonPosition(editableData.value, currentSheet.name, rowIndex, colIndex);
|
|
609
|
-
if (cellStart < 0) return;
|
|
610
|
-
editorTextarea.value.focus();
|
|
611
|
-
editorTextarea.value.setSelectionRange(cellStart, cellStart + cellStr.length);
|
|
612
|
-
// Scroll the textarea to make the selection visible.
|
|
613
|
-
const textBeforeSelection = editableData.value.substring(0, cellStart);
|
|
614
|
-
const lineNumber = textBeforeSelection.split("\n").length;
|
|
615
|
-
const lineHeight = 22;
|
|
616
|
-
const textarea = editorTextarea.value;
|
|
617
|
-
textarea.scrollTop = Math.max(0, lineNumber * lineHeight - textarea.clientHeight / 2);
|
|
618
|
-
} catch (error) {
|
|
619
|
-
console.error("Failed to select cell in editor:", error);
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
async function applyChanges() {
|
|
624
|
-
try {
|
|
625
|
-
// Parse the edited JSON
|
|
626
|
-
const parsedSheets = JSON.parse(editableData.value);
|
|
627
|
-
|
|
628
|
-
// Validate it's an array
|
|
629
|
-
if (!Array.isArray(parsedSheets)) {
|
|
630
|
-
throw new Error(t("pluginSpreadsheet.dataMustBeArray"));
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
// Persist to disk (if file-backed) and emit update
|
|
634
|
-
await persistSheets(parsedSheets);
|
|
635
|
-
|
|
636
|
-
// Reset to first sheet after update
|
|
637
|
-
activeSheetIndex.value = 0;
|
|
638
|
-
} catch (error) {
|
|
639
|
-
alert(t("pluginSpreadsheet.invalidJsonAlert", { error: formatErrorMessage(error, t("pluginSpreadsheet.unknownError")) }));
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
// Watch for external changes to selectedResult
|
|
644
|
-
watch(
|
|
645
|
-
() => props.selectedResult.data?.sheets,
|
|
646
|
-
() => {
|
|
647
|
-
fetchSheets().then(() => {
|
|
648
|
-
editableData.value = JSON.stringify(resolvedSheets.value || [], null, 2);
|
|
649
|
-
// Reset to first sheet when result changes
|
|
650
|
-
activeSheetIndex.value = 0;
|
|
651
|
-
});
|
|
652
|
-
},
|
|
653
|
-
);
|
|
654
|
-
|
|
655
|
-
// Reset active sheet if it's out of bounds
|
|
656
|
-
watch(
|
|
657
|
-
() => resolvedSheets.value?.length,
|
|
658
|
-
(length) => {
|
|
659
|
-
if (length && activeSheetIndex.value >= length) {
|
|
660
|
-
activeSheetIndex.value = 0;
|
|
661
|
-
}
|
|
662
|
-
},
|
|
663
|
-
);
|
|
664
|
-
|
|
665
|
-
// Highlight selected cell and referenced cells when mini editor is
|
|
666
|
-
// open. The per-step DOM work lives in cellHighlights.ts so this
|
|
667
|
-
// callback stays trivial and the complexity lands on the helpers,
|
|
668
|
-
// each of which is linear.
|
|
669
|
-
watch(
|
|
670
|
-
[miniEditorOpen, miniEditorCell, referencedCells, renderedHtml],
|
|
671
|
-
() => {
|
|
672
|
-
clearCellHighlights(tableContainer.value);
|
|
673
|
-
if (!miniEditorOpen.value) return;
|
|
674
|
-
applyCellHighlights(tableContainer.value, miniEditorCell.value, referencedCells.value);
|
|
675
|
-
},
|
|
676
|
-
{ flush: "post" },
|
|
677
|
-
);
|
|
678
|
-
|
|
679
|
-
function isEditableTarget(target: EventTarget | null): boolean {
|
|
680
|
-
if (!(target instanceof HTMLElement)) return false;
|
|
681
|
-
return target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable;
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
function handleKeyboardNavigation(event: KeyboardEvent) {
|
|
685
|
-
if (!miniEditorOpen.value || !miniEditorCell.value) return;
|
|
686
|
-
if (isEditableTarget(event.target)) return;
|
|
687
|
-
|
|
688
|
-
const { row, col } = miniEditorCell.value;
|
|
689
|
-
const next = getArrowKeyOffset(event.key, row, col);
|
|
690
|
-
if (!next) return;
|
|
691
|
-
|
|
692
|
-
try {
|
|
693
|
-
const sheets = JSON.parse(editableData.value);
|
|
694
|
-
if (!isWithinSheetBounds(sheets[activeSheetIndex.value], next.row, next.col)) return;
|
|
695
|
-
event.preventDefault();
|
|
696
|
-
openMiniEditor(next.row, next.col);
|
|
697
|
-
} catch (error) {
|
|
698
|
-
console.error("Failed to navigate cells:", error);
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
// Add keyboard event listener on mount
|
|
703
|
-
onMounted(() => {
|
|
704
|
-
document.addEventListener("keydown", handleKeyboardNavigation);
|
|
705
|
-
});
|
|
706
|
-
|
|
707
|
-
// Remove keyboard event listener on unmount
|
|
708
|
-
onUnmounted(() => {
|
|
709
|
-
document.removeEventListener("keydown", handleKeyboardNavigation);
|
|
710
|
-
});
|
|
711
|
-
</script>
|
|
712
|
-
|
|
713
|
-
<style scoped>
|
|
714
|
-
.spreadsheet-container {
|
|
715
|
-
width: 100%;
|
|
716
|
-
height: 100%;
|
|
717
|
-
display: flex;
|
|
718
|
-
flex-direction: column;
|
|
719
|
-
background: white;
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
.spreadsheet-content-wrapper {
|
|
723
|
-
flex: 1;
|
|
724
|
-
overflow-y: auto;
|
|
725
|
-
min-height: 0;
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
.header {
|
|
729
|
-
display: flex;
|
|
730
|
-
align-items: center;
|
|
731
|
-
justify-content: space-between;
|
|
732
|
-
margin-bottom: 1em;
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
.title {
|
|
736
|
-
font-size: 2em;
|
|
737
|
-
margin: 0;
|
|
738
|
-
font-weight: bold;
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
/* Sheet tabs */
|
|
742
|
-
.sheet-tabs {
|
|
743
|
-
display: flex;
|
|
744
|
-
gap: 0.25em;
|
|
745
|
-
margin-bottom: 1em;
|
|
746
|
-
border-bottom: 2px solid #e0e0e0;
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
.sheet-tab {
|
|
750
|
-
padding: 0.5em 1em;
|
|
751
|
-
background: #f5f5f5;
|
|
752
|
-
border: none;
|
|
753
|
-
border-top-left-radius: 4px;
|
|
754
|
-
border-top-right-radius: 4px;
|
|
755
|
-
cursor: pointer;
|
|
756
|
-
font-size: 0.9em;
|
|
757
|
-
color: #666;
|
|
758
|
-
transition: background-color 0.2s;
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
.sheet-tab:hover {
|
|
762
|
-
background: #e8e8e8;
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
.sheet-tab.active {
|
|
766
|
-
background: white;
|
|
767
|
-
color: #333;
|
|
768
|
-
font-weight: 500;
|
|
769
|
-
border-bottom: 2px solid white;
|
|
770
|
-
margin-bottom: -2px;
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
/* Table container */
|
|
774
|
-
.table-container {
|
|
775
|
-
overflow-x: auto;
|
|
776
|
-
background: white;
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
/* Style the generated table */
|
|
780
|
-
.table-container :deep(table) {
|
|
781
|
-
border-collapse: collapse;
|
|
782
|
-
width: 100%;
|
|
783
|
-
font-family: "Segoe UI", Arial, sans-serif;
|
|
784
|
-
font-size: 0.9em;
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
.table-container :deep(td),
|
|
788
|
-
.table-container :deep(th) {
|
|
789
|
-
border: 1px solid #d0d0d0;
|
|
790
|
-
padding: 0.5em 0.75em;
|
|
791
|
-
text-align: left;
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
.table-container :deep(th) {
|
|
795
|
-
background-color: #f5f5f5;
|
|
796
|
-
font-weight: 600;
|
|
797
|
-
color: #333;
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
.table-container :deep(tr:nth-child(even)) {
|
|
801
|
-
background-color: #fafafa;
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
.table-container :deep(tr:hover) {
|
|
805
|
-
background-color: #f0f0f0;
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
.table-container :deep(.cell-editing) {
|
|
809
|
-
background-color: #e8f5e9 !important;
|
|
810
|
-
outline: 2px solid #217346 !important;
|
|
811
|
-
outline-offset: -2px;
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
.table-container :deep(.cell-referenced) {
|
|
815
|
-
background-color: #fff3e0 !important;
|
|
816
|
-
outline: 2px solid #ff9800 !important;
|
|
817
|
-
outline-offset: -2px;
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
/* Error message */
|
|
821
|
-
.error {
|
|
822
|
-
padding: 1em;
|
|
823
|
-
background: #ffebee;
|
|
824
|
-
color: #c62828;
|
|
825
|
-
border-radius: 4px;
|
|
826
|
-
margin: 1em 0;
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
/* Editor section */
|
|
830
|
-
.spreadsheet-source {
|
|
831
|
-
padding: 0.5rem;
|
|
832
|
-
background: #f5f5f5;
|
|
833
|
-
border-top: 1px solid #e0e0e0;
|
|
834
|
-
font-family: Consolas, "MS Gothic", "BIZ UDGothic", monospace;
|
|
835
|
-
font-size: 0.85rem;
|
|
836
|
-
flex-shrink: 0;
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
.spreadsheet-source summary {
|
|
840
|
-
cursor: pointer;
|
|
841
|
-
user-select: none;
|
|
842
|
-
padding: 0.5rem;
|
|
843
|
-
background: #e8e8e8;
|
|
844
|
-
border-radius: 4px;
|
|
845
|
-
font-weight: 500;
|
|
846
|
-
color: #333;
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
.spreadsheet-source[open] summary {
|
|
850
|
-
margin-bottom: 0.5rem;
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
.spreadsheet-source summary:hover {
|
|
854
|
-
background: #d8d8d8;
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
.spreadsheet-editor {
|
|
858
|
-
width: 100%;
|
|
859
|
-
height: 40vh;
|
|
860
|
-
padding: 1rem;
|
|
861
|
-
background: #ffffff;
|
|
862
|
-
border: 1px solid #ccc;
|
|
863
|
-
border-radius: 4px;
|
|
864
|
-
color: #333;
|
|
865
|
-
font-family: "Courier New", "MS Gothic", "BIZ UDGothic", monospace;
|
|
866
|
-
font-size: 0.9rem;
|
|
867
|
-
resize: vertical;
|
|
868
|
-
margin-bottom: 0.5rem;
|
|
869
|
-
line-height: 1.5;
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
.spreadsheet-editor:focus {
|
|
873
|
-
outline: none;
|
|
874
|
-
border-color: #217346;
|
|
875
|
-
box-shadow: 0 0 0 2px rgba(33, 115, 70, 0.1);
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
.apply-btn {
|
|
879
|
-
padding: 0.5rem 1rem;
|
|
880
|
-
background: #217346;
|
|
881
|
-
color: white;
|
|
882
|
-
border: none;
|
|
883
|
-
border-radius: 4px;
|
|
884
|
-
cursor: pointer;
|
|
885
|
-
font-size: 0.9rem;
|
|
886
|
-
transition: background 0.2s;
|
|
887
|
-
font-weight: 500;
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
.apply-btn:hover {
|
|
891
|
-
background: #1e6a3f;
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
.apply-btn:active {
|
|
895
|
-
background: #1a5c36;
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
.apply-btn:disabled {
|
|
899
|
-
background: #cccccc;
|
|
900
|
-
color: #666666;
|
|
901
|
-
cursor: not-allowed;
|
|
902
|
-
opacity: 0.6;
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
.apply-btn:disabled:hover {
|
|
906
|
-
background: #cccccc;
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
/* Mini Editor Panel */
|
|
910
|
-
.mini-editor-panel {
|
|
911
|
-
background: #f8f8f8;
|
|
912
|
-
border-top: 1px solid #d0d0d0;
|
|
913
|
-
flex-shrink: 0;
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
.mini-editor-content {
|
|
917
|
-
display: flex;
|
|
918
|
-
align-items: center;
|
|
919
|
-
gap: 0.5rem;
|
|
920
|
-
padding: 0.5rem 1rem;
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
.cell-ref {
|
|
924
|
-
font-family: Consolas, "MS Gothic", "BIZ UDGothic", monospace;
|
|
925
|
-
font-weight: 600;
|
|
926
|
-
color: #217346;
|
|
927
|
-
font-size: 0.85rem;
|
|
928
|
-
min-width: 2rem;
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
.radio-group {
|
|
932
|
-
display: flex;
|
|
933
|
-
gap: 0.75rem;
|
|
934
|
-
border-right: 1px solid #d0d0d0;
|
|
935
|
-
padding-right: 0.75rem;
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
.radio-option {
|
|
939
|
-
display: flex;
|
|
940
|
-
align-items: center;
|
|
941
|
-
gap: 0.25rem;
|
|
942
|
-
cursor: pointer;
|
|
943
|
-
font-size: 0.85rem;
|
|
944
|
-
color: #555;
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
.radio-option input[type="radio"] {
|
|
948
|
-
cursor: pointer;
|
|
949
|
-
width: 0.9rem;
|
|
950
|
-
height: 0.9rem;
|
|
951
|
-
}
|
|
952
|
-
|
|
953
|
-
.form-input {
|
|
954
|
-
flex: 1;
|
|
955
|
-
padding: 0.4rem 0.6rem;
|
|
956
|
-
border: 1px solid #ccc;
|
|
957
|
-
border-radius: 3px;
|
|
958
|
-
font-size: 0.85rem;
|
|
959
|
-
font-family: inherit;
|
|
960
|
-
transition: border-color 0.2s;
|
|
961
|
-
min-width: 120px;
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
.form-input:focus {
|
|
965
|
-
outline: none;
|
|
966
|
-
border-color: #217346;
|
|
967
|
-
box-shadow: 0 0 0 2px rgba(33, 115, 70, 0.1);
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
.form-input::placeholder {
|
|
971
|
-
color: #999;
|
|
972
|
-
font-size: 0.8rem;
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
.save-btn,
|
|
976
|
-
.cancel-btn {
|
|
977
|
-
padding: 0.4rem 0.8rem;
|
|
978
|
-
border: none;
|
|
979
|
-
border-radius: 3px;
|
|
980
|
-
cursor: pointer;
|
|
981
|
-
font-size: 0.85rem;
|
|
982
|
-
font-weight: 500;
|
|
983
|
-
transition: background 0.2s;
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
.save-btn {
|
|
987
|
-
background: #217346;
|
|
988
|
-
color: white;
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
.save-btn:hover {
|
|
992
|
-
background: #1e6a3f;
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
.cancel-btn {
|
|
996
|
-
background: transparent;
|
|
997
|
-
color: #666;
|
|
998
|
-
padding: 0.4rem;
|
|
999
|
-
font-size: 1.2rem;
|
|
1000
|
-
line-height: 1;
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
.cancel-btn:hover {
|
|
1004
|
-
color: #333;
|
|
1005
|
-
background: #e0e0e0;
|
|
1006
|
-
}
|
|
1007
|
-
</style>
|