@a3s-lab/office 0.41.0 → 0.43.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/README.md CHANGED
@@ -309,6 +309,36 @@ and [CLI reference](./docs/latest/en/cli-reference.md).
309
309
 
310
310
  ## Current release
311
311
 
312
+ Version `0.43.0` makes Spreadsheet formula conditional formatting a local,
313
+ editable workflow instead of a file-only preservation path:
314
+
315
+ - **Home → Conditional Formatting → Custom formula** authors rules with
316
+ relative/absolute references, finite cross-sheet references, independent
317
+ text/fill colors, ordered precedence, and Stop-if-true behavior. Imported
318
+ XLSX expression rules reopen in the same editor.
319
+ - The shared synchronous evaluator reads only cached workbook values, caps
320
+ formulas at 255 Unicode characters and each decision at 1,024 cells, scans
321
+ bounded blank ranges without densifying the workbook, and fails closed for
322
+ external, whole-row/column, missing-sheet, or uncached-formula references.
323
+ - Native XLSX differential styles, `sqref`, priorities, and formulas round-trip
324
+ through export/reopen. The public **新建 → 公式条件格式** template shows a
325
+ blocking rule and a cross-sheet threshold without any remote service.
326
+
327
+ Version `0.42.0` extends Spreadsheet data validation with a local custom-formula
328
+ workflow that remains bounded, inspectable, and native-file compatible:
329
+
330
+ - Author a formula with an optional `=` prefix in the same accessible dialog;
331
+ relative references are anchored per selected range and the proposed value
332
+ is evaluated before it enters the controlled workbook.
333
+ - Common local functions, cell/range references, and sheet-qualified references
334
+ are evaluated synchronously with a 255-character formula and 1,024-cell read
335
+ budget. External, whole-row/column, missing-sheet, and uncached-formula
336
+ references fail closed instead of silently accepting an unsafe edit.
337
+ - Custom rules round-trip through XLSX and are demonstrated in the public
338
+ **新建 → 数据验证** template, alongside the existing list, date, and alert
339
+ branches. Paste and object-level batch writes remain separate preflight
340
+ boundaries.
341
+
312
342
  Version `0.41.0` aligns Spreadsheet data-validation alerts with the local,
313
343
  testable interaction model used by Traditional Office:
314
344
 
package/dist/0~7048.js CHANGED
@@ -2733,4 +2733,4 @@ function abortError() {
2733
2733
  function disposedError() {
2734
2734
  return new Error('Office kernel client is disposed.');
2735
2735
  }
2736
- export { CollectionState, CommittedOfficeNumberField, OfficeCheckbox, OfficeNumberField, createOfficeKernelClient, handleOfficeTaskPaneKeyDown, normalizeOfficeFontFamily, officeFontFamilies, officeFontFamilyLabel, useOfficeDialog, useOfficeTaskPaneEscape, useOfficeTaskPaneModal };
2736
+ export { CollectionState, CommittedOfficeNumberField, OfficeCheckbox, OfficeNumberField, createOfficeKernelClient, handleOfficeTaskPaneKeyDown, normalizeFormulaForFortuneParser, normalizeOfficeFontFamily, officeFontFamilies, officeFontFamilyLabel, useOfficeDialog, useOfficeTaskPaneEscape, useOfficeTaskPaneModal };
@@ -3,13 +3,14 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import { Workbook } from "@fortune-sheet/react";
4
4
  import { AArrowDown, AArrowUp, AlignCenter, AlignLeft, AlignRight, AlignVerticalJustifyCenter, AlignVerticalJustifyEnd, AlignVerticalJustifyStart, ArrowDown, ArrowDownToLine, ArrowLeft, ArrowLeftToLine, ArrowRight, ArrowRightToLine, ArrowUp, ArrowUpToLine, BadgeJapaneseYen, BarChart3, BetweenHorizontalEnd, BetweenHorizontalStart, BetweenVerticalEnd, BetweenVerticalStart, Bold, Bookmark, Calculator, CalendarClock, CalendarDays, Check, ChevronDown, ChevronLeft, ChevronRight, ClipboardPaste, Clock, Cloud, Columns3, Copy, CopyPlus, DecimalsArrowLeft, DecimalsArrowRight, Eraser, Eye, EyeOff, FileX2, Globe2, Grid2X2, Grid3X3, Hash, Info, Italic, KeyRound, Layers2, Link2, Link2Off, ListChecks, ListFilter, ListOrdered, ListPlus, LocateFixed, Merge, MessageSquareX, MoveHorizontal, MoveVertical, Paintbrush, Palette, PanelLeft, PanelTop, PanelsTopLeft, Pencil, Percent, Plus, Printer, Redo2, RefreshCw, Rows3, Save, Scissors, Search, Settings2, ShieldCheck, Sigma, SlidersHorizontal, SortAsc, SortDesc, Strikethrough, SwatchBook, Table2, TableProperties, Trash2, TriangleAlert, Underline, Undo2, WrapText, X } from "lucide-react";
5
5
  import { Fragment as external_react_Fragment, cloneElement, createElement, memo, useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
6
+ import { Parser, SUPPORTED_FORMULAS } from "@fortune-sheet/formula-parser";
6
7
  import { convertSpanToShareString, genarate, indexToColumnChar, update as core_update } from "@fortune-sheet/core";
8
+ import { useOfficeTaskPaneEscape, OfficeCheckbox, useOfficeDialog, CommittedOfficeNumberField, useOfficeTaskPaneModal, createOfficeKernelClient, officeFontFamilies, CollectionState, OfficeNumberField, handleOfficeTaskPaneKeyDown, normalizeFormulaForFortuneParser } from "./0~7048.js";
7
9
  import { freezeImportedSpreadsheetCell, normalizeXlsxSemanticColorOrigin, spreadsheetMatrixProfile, withXlsxPatternFill, withoutEditableRange, normalizeSpreadsheetConditionalVisualOptions, xlsxSemanticColorMatchesValue, withXlsxGradientFill, effectiveSpreadsheetPageSetup, attachSpreadsheetShownCommentCells, normalizeXlsxPatternFill, withEditableRange, sameSpreadsheetHistoryValue, isSpreadsheetConditionalComparisonOperator, spreadsheetConditionalIconForValue, defaultSpreadsheetConditionalIconThresholds, xlsxPatternFillTypes, sheetProtectionAuthority, normalizeXlsxGradientFill, xlsxGradientFillFallbackColor, editableRangeCellCount, unlockedCellCount, registerDerivedSpreadsheetMatrix, editableRangeRequiresCredentials, SPREADSHEET_CONDITIONAL_COMPARISON_OPERATORS, normalizeSpreadsheetConditionalIconSetFormat, defaultSpreadsheetDataBarOptions, spreadsheetConditionalThresholdValue, normalizeSheetProtectionAuthority, withSheetProtection, spreadsheetProtectionKey, defaultSpreadsheetColorScaleThresholds, protectedSheetCount, SPREADSHEET_CONDITIONAL_ICON_SETS, withSheetSelectionPermissions, spreadsheetConditionalComparisonNeedsUpperValue, drawSpreadsheetConditionalIcon, isSpreadsheetConditionalIconSetName, spreadsheetConditionalThresholdsEqual, spreadsheetConditionalIconSetCount, spreadsheetConditionalIconAppearance } from "./9333.js";
8
10
  import { workSpreadsheetChartSupportsGrouping, spreadsheetFormulaRangesForSelection, createSpreadsheetChartFromSelection, normalizeWorkSpreadsheetDataLabelPosition, normalizeSpreadsheetPrintTitleColumns, normalizeSpreadsheetPrintTitleRows, formulaHasStructuredReference, normalizeWorkSpreadsheetErrorBars, defaultPivotValueCaption, workSpreadsheetChartSupportsBarSpacing, normalizeWorkSpreadsheetChartGrouping, spreadsheetGridSize, normalizeWorkSpreadsheetChartAxisGroup, normalizeWorkSpreadsheetScatterStyle, cloneSparseMatrix, spreadsheetPivotAggregationLabel, workSpreadsheetChartAxisIsCategoryAxis, reconcileSpreadsheetChartPreviews, normalizeWorkSpreadsheetChartAxes, normalizeWorkSpreadsheetDoughnutHoleSize, normalizeWorkSpreadsheetChartLegendPosition, spreadsheetChartCount, workSpreadsheetChartTypeLabel, sparseArrayEntries, normalizeWorkSpreadsheetTrendline, effectiveSpreadsheetCalculationSettings, spreadsheetPivotIntersects, normalizeWorkSpreadsheetDataLabels, spreadsheetPivotOutputContains, normalizeSpreadsheetPrintArea, spreadsheetCellAddress, formatSpreadsheetCellRanges, normalizeWorkSpreadsheetChartLayout, spreadsheetFormulaRangeForCell, workSpreadsheetChartAxisIsValueAxis, normalizeWorkSpreadsheetChartSmoothLines, spreadsheetPivotFilterValueKey, normalizeWorkSpreadsheetBubbleScale, normalizeWorkSpreadsheetChartSeriesStyle, volatileSpreadsheetFormulaFunctions, sparseArrayIndexes, normalizeWorkSpreadsheetBubbleSizeRepresents, workSpreadsheetChartSupportsTrendlines, normalizeWorkSpreadsheetChartLegendOverlay, parseSpreadsheetChartReference, workSpreadsheetChartGroupingIsStacked, normalizeWorkSpreadsheetChartGapWidth, workSpreadsheetChartSupportsSmoothLines, refreshSpreadsheetPivotTables, createSpreadsheetPivotFromSelection, reconcileSpreadsheetPivots, spreadsheetPivotCount, sparseMatrixColumnCount, workSpreadsheetChartSupportsErrorBars, formulaHasExternalReference, isValidSpreadsheetDefinedName, spreadsheetPivotValidation, deleteSpreadsheetPivotTable, workSpreadsheetChartAxisDefaultLabelPosition, workSpreadsheetChartSupportsSeriesAnalysis, parseSpreadsheetCellRanges, spreadsheetPivotFilterItems, spreadsheetPivotFields, normalizeWorkSpreadsheetChartOverlap, spreadsheetFormulaRangeConflict, spreadsheetSheetsWithChartPreviews, workSpreadsheetChartUsesNumericXAxis, workSpreadsheetChartAxisShowsMajorGridlinesByDefault, workSpreadsheetChartSupportsAxes, normalizeWorkSpreadsheetCombinationSeriesType, normalizeWorkSpreadsheetRadarStyle } from "./8715.js";
9
11
  import { workSpreadsheetFilterHiddenRows, xlsxRichTextCellText, spreadsheetTextOrientationFromAngle, spreadsheetCellValueWithDiagonalBorder, spreadsheetUnderlineCellValue, workSpreadsheetAutoFilterCriteriaEntries, spreadsheetTableTotalsFunctionLabel, withDefaultSpreadsheetTableTotalsLabel, normalizeXlsxRichTextEditSource, spreadsheetTableTotalsColumnPatch, SPREADSHEET_TABLE_TOTALS_FUNCTIONS, normalizeXlsxRichTextCell, activeXlsxNativeFill, normalizeSpreadsheetDateValidationBoundary, spreadsheetTextOrientationChoiceFromCell, parseSpreadsheetSortCustomList, workSpreadsheetFilterTextCharacters, sameXlsxRichTextRunStyle, spreadsheetSortCustomListMatchKey, createSpreadsheetSortCustomList, reconcileSpreadsheetTableTotalsColumns, spreadsheetTextOrientationFromCell, patchSpreadsheetRichTextFontRuns, synchronizeSpreadsheetTableTotalsRow, workSpreadsheetAutoFilterManuallyHiddenRows, normalizedWorkSpreadsheetAutoFilterRange, workSpreadsheetSheetWithoutAutoFilterCriteria, isSpreadsheetTextOrientationId, reconcileSpreadsheetTableCalculatedColumns, spreadsheetVisibleTextRotationFromCell, spreadsheetTextOrientationFromChoice, normalizeWorkSpreadsheetFilterCriteria, readOfficeSpreadsheetCollaboration as readWorkOfficeSpreadsheetCollaboration, normalizeSpreadsheetDataValidationErrorStyle, workSpreadsheetAutoFilterCriteria, normalizeXlsxRichTextColor, isHighSurrogate, xlsxNativeFillCellKeys, workSpreadsheetSheetWithReappliedAutoFilterCriteria, workSpreadsheetSheetWithAutoFilterCriteria, createOfficeSpreadsheetCollaborationBinding as createWorkOfficeSpreadsheetCollaborationBinding, boundedSpreadsheetDataValidationText, spreadsheetUnderlineStyle, workSpreadsheetCellIsDate, workSpreadsheetAutoFilterOwnedRows, deleteXlsxNativeFills, spreadsheetTextOrientationCellStyle, normalizeXlsxRichTextRun, spreadsheetDiagonalBorderFromCellValue, isLowSurrogate, mergeSpreadsheetSortCustomLists, spreadsheetTableTotalsRowHasContent, validateSpreadsheetSortCustomList, normalizeStoredSpreadsheetSortCustomLists, coalesceXlsxRichTextRuns, fillSpreadsheetTableCalculatedColumns, replaceOfficeSpreadsheetCollaboration as replaceWorkOfficeSpreadsheetCollaboration, validXlsxRichText, spreadsheetSortCustomListsEqual, isSpreadsheetUnderlineStyle } from "./5416.js";
10
12
  import { moveOfficeMenuFocus, stepOfficeZoom, useOfficeRemoteParticipants, Popover, Dialog, officePresenceColor, useOfficePublishPresenceLocation, button_Button, icon_button_IconButton, moveOfficeGridMenuFocus, office_text_field_OfficeTextField, useDialogFocusScope, useOfficeEditorWheelZoom } from "./0~8136.js";
11
13
  import { isOfficeKernelSpreadsheetError, normalizeCssColor, OFFICE_KERNEL_SPREADSHEET_MAX_ROWS, createOfficeId as createWorkId } from "./4121.js";
12
- import { useOfficeTaskPaneEscape, OfficeCheckbox, CommittedOfficeNumberField, useOfficeDialog, useOfficeTaskPaneModal, createOfficeKernelClient, officeFontFamilies, CollectionState, OfficeNumberField, handleOfficeTaskPaneKeyDown } from "./0~7048.js";
13
14
  import { isOfficeShortcutBlocked, OfficeColorPicker } from "./0~3635.js";
14
15
  import { createOfficeEditorExtension, useOfficeEditorRuntime, useOfficeEditorKeyboardShortcuts } from "./0~3557.js";
15
16
  import { showToast } from "./6489.js";
@@ -158,30 +159,187 @@ function displayLength(valueRatio, minLength, maxLength) {
158
159
  const normalized = Math.max(0, Math.min(1, valueRatio));
159
160
  return minLength + (maxLength - minLength) * normalized;
160
161
  }
161
- function spreadsheetConditionalFormatStyles(sheet) {
162
+ const MAX_SPREADSHEET_LOCAL_FORMULA_LENGTH = 255;
163
+ const work_spreadsheet_local_formula_MAX_SPREADSHEET_LOCAL_FORMULA_REFERENCED_CELLS = 1024;
164
+ const MAX_SHEET_ROW = 1048576;
165
+ const MAX_SHEET_COLUMN = 16384;
166
+ const LOCAL_FORMULA_FUNCTIONS = new Set(SUPPORTED_FORMULAS.map((name)=>name.toUpperCase()));
167
+ function evaluateSpreadsheetLocalFormula(formula, options) {
168
+ const source = formula.trim().replace(/^=/, '').trim();
169
+ if (!source) return unsupportedLocalFormula('公式为空。');
170
+ if (Array.from(source).length > MAX_SPREADSHEET_LOCAL_FORMULA_LENGTH) return unsupportedLocalFormula('公式超过 255 个字符。');
171
+ const currentSheet = options.sheets.find((candidate)=>candidate.id === options.sheetId);
172
+ if (!currentSheet) return unsupportedLocalFormula('找不到公式所在的工作表。');
173
+ const evaluator = new SpreadsheetLocalFormulaEvaluator(options, currentSheet);
174
+ return evaluator.evaluate(source);
175
+ }
176
+ function spreadsheetLocalFormulaBoolean(value) {
177
+ if ('boolean' == typeof value) return value;
178
+ if ('number' == typeof value) return Number.isFinite(value) && 0 !== value;
179
+ return 'string' == typeof value && 'TRUE' === value.trim().toUpperCase();
180
+ }
181
+ class SpreadsheetLocalFormulaEvaluator {
182
+ options;
183
+ currentSheet;
184
+ sheetsByName = new Map();
185
+ anchor;
186
+ referencedCells = 0;
187
+ unsupportedMessage;
188
+ celldataBySheet = new Map();
189
+ constructor(options, currentSheet){
190
+ this.options = options;
191
+ this.currentSheet = currentSheet;
192
+ this.anchor = {
193
+ row: options.anchorRow ?? options.row,
194
+ column: options.anchorColumn ?? options.column
195
+ };
196
+ for (const sheet of options.sheets)this.sheetsByName.set(sheet.name.trim().toLocaleLowerCase(), sheet);
197
+ }
198
+ evaluate(formula) {
199
+ const parser = new Parser();
200
+ parser.setFunction('ROW', (parameters)=>parameters.length ? null : this.options.row + 1).setFunction('COLUMN', (parameters)=>parameters.length ? null : this.options.column + 1);
201
+ let unsupportedFunction;
202
+ parser.on('callFunction', (name)=>{
203
+ const normalized = name.toUpperCase();
204
+ if (!LOCAL_FORMULA_FUNCTIONS.has(normalized)) unsupportedFunction ??= normalized;
205
+ });
206
+ parser.on('callCellValue', (coordinate, _parserOptions, done)=>{
207
+ const resolved = this.resolveCoordinate(this.currentSheet, coordinate);
208
+ if (!resolved) return void done(null);
209
+ done(this.readCell(resolved));
210
+ });
211
+ parser.on('callRangeValue', (start, end, _parserOptions, done)=>{
212
+ done(this.readRange(start, end));
213
+ });
214
+ const parsed = parser.parse(normalizeFormulaForFortuneParser(formula), {
215
+ sheetId: this.currentSheet.id
216
+ });
217
+ if (unsupportedFunction) return unsupportedLocalFormula(`公式函数“${unsupportedFunction}”无法在浏览器本地求值。`);
218
+ if (this.unsupportedMessage) return unsupportedLocalFormula(this.unsupportedMessage);
219
+ if (parsed.error || void 0 === parsed.result) return unsupportedLocalFormula('公式无法求值,请检查函数、引用和括号。');
220
+ return {
221
+ supported: true,
222
+ value: parsed.result
223
+ };
224
+ }
225
+ resolveCoordinate(fallbackSheet, coordinate) {
226
+ const sheet = coordinate.sheetName ? this.sheetsByName.get(coordinate.sheetName.trim().toLocaleLowerCase()) : fallbackSheet;
227
+ if (!sheet) {
228
+ this.unsupportedMessage = '公式引用了不存在的工作表。';
229
+ return null;
230
+ }
231
+ if (coordinate.row.index < 0 || coordinate.column.index < 0) {
232
+ this.unsupportedMessage = '公式暂不支持整行或整列引用,请改用有限区域。';
233
+ return null;
234
+ }
235
+ const row = coordinate.row.isAbsolute ? coordinate.row.index : this.options.row + (coordinate.row.index - this.anchor.row);
236
+ const column = coordinate.column.isAbsolute ? coordinate.column.index : this.options.column + (coordinate.column.index - this.anchor.column);
237
+ if (row < 0 || column < 0 || row >= MAX_SHEET_ROW || column >= MAX_SHEET_COLUMN) {
238
+ this.unsupportedMessage = '公式引用超出了工作表边界。';
239
+ return null;
240
+ }
241
+ return {
242
+ sheet,
243
+ row,
244
+ column
245
+ };
246
+ }
247
+ readRange(start, end) {
248
+ const startCoordinate = this.resolveCoordinate(this.currentSheet, start);
249
+ const rangeSheet = start.sheetName ? this.sheetsByName.get(start.sheetName.trim().toLocaleLowerCase()) : this.currentSheet;
250
+ const endCoordinate = rangeSheet ? this.resolveCoordinate(rangeSheet, end) : null;
251
+ if (!startCoordinate || !endCoordinate || startCoordinate.sheet.id !== endCoordinate.sheet.id) {
252
+ this.unsupportedMessage ??= '公式区域引用必须位于同一工作表。';
253
+ return [];
254
+ }
255
+ const rowStart = Math.min(startCoordinate.row, endCoordinate.row);
256
+ const rowEnd = Math.max(startCoordinate.row, endCoordinate.row);
257
+ const columnStart = Math.min(startCoordinate.column, endCoordinate.column);
258
+ const columnEnd = Math.max(startCoordinate.column, endCoordinate.column);
259
+ const area = (rowEnd - rowStart + 1) * (columnEnd - columnStart + 1);
260
+ if (area <= 0 || area > work_spreadsheet_local_formula_MAX_SPREADSHEET_LOCAL_FORMULA_REFERENCED_CELLS - this.referencedCells) {
261
+ this.unsupportedMessage = '公式一次最多读取 1,024 个单元格。';
262
+ return [];
263
+ }
264
+ const values = [];
265
+ for(let row = rowStart; row <= rowEnd; row += 1){
266
+ const valuesRow = [];
267
+ for(let column = columnStart; column <= columnEnd; column += 1)valuesRow.push(this.readCell({
268
+ sheet: startCoordinate.sheet,
269
+ row,
270
+ column
271
+ }));
272
+ values.push(valuesRow);
273
+ }
274
+ return values;
275
+ }
276
+ readCell(coordinate) {
277
+ this.referencedCells += 1;
278
+ if (this.referencedCells > work_spreadsheet_local_formula_MAX_SPREADSHEET_LOCAL_FORMULA_REFERENCED_CELLS) {
279
+ this.unsupportedMessage = '公式一次最多读取 1,024 个单元格。';
280
+ return null;
281
+ }
282
+ if (this.options.useProposedValue && coordinate.sheet.id === this.currentSheet.id && coordinate.row === this.options.row && coordinate.column === this.options.column) return this.options.proposedValue;
283
+ const cell = this.cellAt(coordinate.sheet, coordinate.row, coordinate.column);
284
+ if (!cell) return null;
285
+ if ('string' == typeof cell.f && cell.f.length > 0 && void 0 === cell.v) {
286
+ this.unsupportedMessage ??= '公式引用了尚未计算的公式单元格。';
287
+ return null;
288
+ }
289
+ return cell.v ?? null;
290
+ }
291
+ cellAt(sheet, row, column) {
292
+ if (void 0 !== sheet.data) return sheet.data[row]?.[column] ?? null;
293
+ let cells = this.celldataBySheet.get(sheet.id);
294
+ if (!cells) {
295
+ cells = new Map();
296
+ for (const entry of sheet.celldata ?? []){
297
+ const value = entry.v && 'object' == typeof entry.v && 'v' in entry.v ? entry.v : {
298
+ v: entry.v
299
+ };
300
+ cells.set(`${entry.r}_${entry.c}`, value);
301
+ }
302
+ this.celldataBySheet.set(sheet.id, cells);
303
+ }
304
+ return cells.get(`${row}_${column}`) ?? null;
305
+ }
306
+ }
307
+ function unsupportedLocalFormula(message) {
308
+ return {
309
+ supported: false,
310
+ value: false,
311
+ message
312
+ };
313
+ }
314
+ const MAX_SPREADSHEET_CONDITIONAL_FORMULA_SCAN_CELLS = 10000;
315
+ function spreadsheetConditionalFormatStyles(sheet, formulaSheets) {
162
316
  const styles = new Map();
163
317
  const data = sheet.data ?? [];
164
- if (!data.length) return styles;
318
+ const rules = conditionalRules(sheet);
319
+ if (!data.length && !rules.some(isFormulaConditionalRule)) return styles;
165
320
  const blocked = new Set();
166
- for (const rule of conditionalRules(sheet)){
321
+ for (const rule of rules){
167
322
  const matched = new Set();
168
323
  if ('colorGradation' === rule.type) applyColorScale(styles, data, rule, blocked, matched);
169
324
  else if ('dataBar' === rule.type) applyDataBars(styles, data, rule, blocked, matched);
170
325
  else if ('icons' === rule.type) applyIconSet(styles, data, rule, blocked, matched);
171
- else applyDefaultRule(styles, data, rule, blocked, matched);
326
+ else applyDefaultRule(styles, data, rule, blocked, matched, formulaSheets, sheet);
172
327
  if (rule.stopIfTrue) for (const key of matched)blocked.add(key);
173
328
  }
174
329
  return styles;
175
330
  }
176
- function applyDefaultRule(styles, data, rule, blocked, matched) {
331
+ function isFormulaConditionalRule(rule) {
332
+ return 'default' === rule.type && 'formula' === rule.conditionName;
333
+ }
334
+ function applyDefaultRule(styles, data, rule, blocked, matched, formulaSheets, currentSheet) {
177
335
  if ('default' !== rule.type || !isConditionalFormatStyle(rule.format) || !rule.conditionName) return;
178
336
  const values = (rule.conditionValue ?? []).map(String);
179
337
  const style = rule.format;
180
338
  if ('formula' === rule.conditionName && values[0]) {
181
- for (const range of rule.cellrange)forEachCell(data, [
339
+ for (const range of rule.cellrange)forEachFormulaCell(data, [
182
340
  range
183
341
  ], ({ row, column })=>{
184
- if (evaluateFormula(values[0], row, column, range, data)) applyDifferentialStyle(styles, blocked, matched, row, column, style);
342
+ if (evaluateFormula(values[0], row, column, range, currentSheet, formulaSheets)) applyDifferentialStyle(styles, blocked, matched, row, column, style);
185
343
  });
186
344
  return;
187
345
  }
@@ -360,35 +518,20 @@ function matchesCondition(value, name, conditions) {
360
518
  }
361
519
  return false;
362
520
  }
363
- function evaluateFormula(source, row, column, baseRange, data) {
364
- const formula = source.trim().replace(/^=/, '').replace(/\s+/g, '');
365
- if (/^TRUE\(\)?$/i.test(formula)) return true;
366
- if (/^FALSE\(\)?$/i.test(formula)) return false;
367
- const parity = /^MOD\((ROW|COLUMN)\(\),(\d+)\)(=|<>|>=|<=|>|<)(-?\d+)$/i.exec(formula);
368
- if (parity) {
369
- const value = 'ROW' === parity[1].toUpperCase() ? row + 1 : column + 1;
370
- return compareWithOperator(value % Number(parity[2]), parity[3], Number(parity[4]));
371
- }
372
- const blank = /^(NOT\()?ISBLANK\((\$?)([A-Z]{1,3})(\$?)([1-9]\d*)\)\)?$/i.exec(formula);
373
- if (blank) {
374
- const target = formulaReference(blank[2], blank[3], blank[4], blank[5], row, column, baseRange);
375
- const empty = null == work_spreadsheet_conditional_format_cellValue(data, target.row, target.column) || '' === work_spreadsheet_conditional_format_cellValue(data, target.row, target.column);
376
- return blank[1] ? !empty : empty;
377
- }
378
- const comparison = /^(\$?)([A-Z]{1,3})(\$?)([1-9]\d*)(=|<>|>=|<=|>|<)(?:"((?:[^"]|"")*)"|(-?\d+(?:\.\d+)?))$/i.exec(formula);
379
- if (!comparison) return false;
380
- const target = formulaReference(comparison[1], comparison[2], comparison[3], comparison[4], row, column, baseRange);
381
- const left = work_spreadsheet_conditional_format_cellValue(data, target.row, target.column);
382
- const right = void 0 !== comparison[6] ? comparison[6].replaceAll('""', '"') : Number(comparison[7]);
383
- return compareWithOperator(left, comparison[5], right);
384
- }
385
- function formulaReference(absoluteColumn, columnName, absoluteRow, rowNumber, row, column, baseRange) {
386
- const sourceColumn = decodeColumn(columnName);
387
- const sourceRow = Number(rowNumber) - 1;
388
- return {
389
- row: absoluteRow ? sourceRow : sourceRow + row - baseRange.row[0],
390
- column: absoluteColumn ? sourceColumn : sourceColumn + column - baseRange.column[0]
391
- };
521
+ function evaluateFormula(source, row, column, baseRange, currentSheet, formulaSheets) {
522
+ const sheets = [
523
+ toFormulaSheet(currentSheet),
524
+ ...(formulaSheets ?? []).filter((sheet)=>sheet.id && sheet.id !== currentSheet.id).map(toFormulaSheet)
525
+ ];
526
+ const result = evaluateSpreadsheetLocalFormula(source, {
527
+ sheets,
528
+ sheetId: currentSheet.id ?? '',
529
+ row,
530
+ column,
531
+ anchorRow: baseRange.row[0],
532
+ anchorColumn: baseRange.column[0]
533
+ });
534
+ return result.supported && spreadsheetLocalFormulaBoolean(result.value);
392
535
  }
393
536
  function applyDifferentialStyle(styles, blocked, matched, row, column, style) {
394
537
  const patch = {};
@@ -447,6 +590,21 @@ function forEachCell(data, ranges, visit) {
447
590
  column
448
591
  });
449
592
  }
593
+ function forEachFormulaCell(data, ranges, visit) {
594
+ for (const range of ranges){
595
+ const area = (range.row[1] - range.row[0] + 1) * (range.column[1] - range.column[0] + 1);
596
+ if (area > 0 && area <= MAX_SPREADSHEET_CONDITIONAL_FORMULA_SCAN_CELLS) {
597
+ for(let row = range.row[0]; row <= range.row[1]; row += 1)for(let column = range.column[0]; column <= range.column[1]; column += 1)visit({
598
+ row,
599
+ column
600
+ });
601
+ continue;
602
+ }
603
+ forEachCell(data, [
604
+ range
605
+ ], visit);
606
+ }
607
+ }
450
608
  function work_spreadsheet_conditional_format_cellValue(data, row, column) {
451
609
  return data[row]?.[column]?.v ?? null;
452
610
  }
@@ -463,19 +621,13 @@ function compareValues(left, right) {
463
621
  if (String(left).trim() && String(right).trim() && Number.isFinite(leftNumber) && Number.isFinite(rightNumber)) return leftNumber === rightNumber ? 0 : leftNumber > rightNumber ? 1 : -1;
464
622
  return String(left).localeCompare(String(right));
465
623
  }
466
- function compareWithOperator(left, operator, right) {
467
- const comparison = compareValues(left, right);
468
- if ('=' === operator) return 0 === comparison;
469
- if ('<>' === operator) return 0 !== comparison;
470
- if ('>' === operator) return comparison > 0;
471
- if ('<' === operator) return comparison < 0;
472
- if ('>=' === operator) return comparison >= 0;
473
- return '<=' === operator && comparison <= 0;
474
- }
475
- function decodeColumn(value) {
476
- let result = 0;
477
- for (const character of value.toUpperCase())result = 26 * result + character.charCodeAt(0) - 64;
478
- return result - 1;
624
+ function toFormulaSheet(sheet) {
625
+ return {
626
+ id: sheet.id ?? '',
627
+ name: sheet.name,
628
+ data: sheet.data,
629
+ celldata: sheet.celldata
630
+ };
479
631
  }
480
632
  function drawSpreadsheetDiagonalDownBorder(context, bounds, line) {
481
633
  drawSpreadsheetDiagonalBorder(context, bounds, line, 'down');
@@ -8036,7 +8188,7 @@ const dateOperators = [
8036
8188
  'noLaterThan'
8037
8189
  ];
8038
8190
  function spreadsheetDataValidationOperators(type) {
8039
- if ('dropdown' === type) return [];
8191
+ if ('custom' === type || 'dropdown' === type) return [];
8040
8192
  return 'date' === type ? dateOperators : numericOperators;
8041
8193
  }
8042
8194
  function createSpreadsheetDataValidationDialogSource(content, target) {
@@ -8121,8 +8273,8 @@ function spreadsheetDataValidationTargetError(sheet, ranges) {
8121
8273
  }
8122
8274
  function normalizeSpreadsheetDataValidationItem(content, sheet, ranges, value) {
8123
8275
  const operators = spreadsheetDataValidationOperators(value.type);
8124
- const type2 = 'dropdown' === value.type ? '' : value.type2;
8125
- if ('dropdown' !== value.type && !operators.includes(type2)) return spreadsheetDataValidationError('invalid-operator');
8276
+ const type2 = 'dropdown' === value.type || 'custom' === value.type ? '' : value.type2;
8277
+ if ('dropdown' !== value.type && 'custom' !== value.type && !operators.includes(type2)) return spreadsheetDataValidationError('invalid-operator');
8126
8278
  const normalizedValues = normalizeSpreadsheetDataValidationValues(content, sheet, value.type, type2, value.value1, value.value2);
8127
8279
  if (!normalizedValues.ok) return normalizedValues;
8128
8280
  return {
@@ -8153,6 +8305,15 @@ function normalizeSpreadsheetDataValidationValues(content, sheet, type, type2, r
8153
8305
  const value2 = rawValue2.trim();
8154
8306
  if (!value1) return spreadsheetDataValidationError('missing-value');
8155
8307
  if ('dropdown' === type) return normalizeSpreadsheetListValidation(content, sheet, value1);
8308
+ if ('custom' === type) {
8309
+ const formula = value1.replace(/^=/, '').trim();
8310
+ if (!formula || Array.from(formula).length > 255 || /[\u0000-\u001f\u007f]/u.test(formula)) return spreadsheetDataValidationError('invalid-custom-formula');
8311
+ return {
8312
+ ok: true,
8313
+ value1: formula,
8314
+ value2: ''
8315
+ };
8316
+ }
8156
8317
  const needsSecond = 'between' === type2 || 'notBetween' === type2;
8157
8318
  if (needsSecond && !value2) return spreadsheetDataValidationError('missing-value');
8158
8319
  if ('date' === type) {
@@ -8256,6 +8417,7 @@ function spreadsheetDataValidationDialogValue(value) {
8256
8417
  }
8257
8418
  function spreadsheetDataValidationType(value) {
8258
8419
  return [
8420
+ 'custom',
8259
8421
  'date',
8260
8422
  'dropdown',
8261
8423
  'number',
@@ -8443,6 +8605,7 @@ function cloneSpreadsheetCellRange(range) {
8443
8605
  }
8444
8606
  function spreadsheetDataValidationError(code) {
8445
8607
  const messages = {
8608
+ 'invalid-custom-formula': '自定义公式必须是 255 个字符以内的本地公式,且不能包含控制字符。',
8446
8609
  'invalid-date': '请输入有效的日期、Excel 日期序号或 DATE(...) 表达式。',
8447
8610
  'invalid-list-source': '请输入不超过 255 个字符的逗号分隔列表,或有效的单行/单列区域。',
8448
8611
  'invalid-number': '请输入有效的数字;整数验证不能使用小数边界。',
@@ -13061,6 +13224,11 @@ function newConditionalRuleDraft(sheetId) {
13061
13224
  comparisonCellColor: DEFAULT_COLORS.comparisonCell,
13062
13225
  comparisonUseTextColor: true,
13063
13226
  comparisonUseCellColor: true,
13227
+ formula: '=A1>0',
13228
+ formulaTextColor: DEFAULT_COLORS.comparisonText,
13229
+ formulaCellColor: DEFAULT_COLORS.comparisonCell,
13230
+ formulaUseTextColor: true,
13231
+ formulaUseCellColor: true,
13064
13232
  scaleSize: '3',
13065
13233
  minimumColor: DEFAULT_COLORS.minimum,
13066
13234
  midpointColor: DEFAULT_COLORS.midpoint,
@@ -13092,7 +13260,7 @@ function conditionalRuleDraftForRule(sheetId, rule) {
13092
13260
  draft.comparisonUseCellColor = Boolean(style.cellColor);
13093
13261
  draft.comparisonTextColor = colorInputValue(style.textColor, DEFAULT_COLORS.comparisonText);
13094
13262
  draft.comparisonCellColor = colorInputValue(style.cellColor, DEFAULT_COLORS.comparisonCell);
13095
- } else if (isToolbarConditionalRule(rule)) {
13263
+ } else if ('default' !== rule.type || 'formula' !== rule.conditionName || Array.isArray(rule.format)) if (isToolbarConditionalRule(rule)) {
13096
13264
  draft.type = 'toolbarRule';
13097
13265
  draft.preservedRule = rule;
13098
13266
  } else if ('colorGradation' === rule.type && Array.isArray(rule.format)) {
@@ -13127,6 +13295,16 @@ function conditionalRuleDraftForRule(sheetId, rule) {
13127
13295
  draft.barMaxLength = String(options.maxLength ?? 90);
13128
13296
  }
13129
13297
  }
13298
+ else {
13299
+ const style = rule.format;
13300
+ const values = (rule.conditionValue ?? []).map(String);
13301
+ draft.type = 'formula';
13302
+ draft.formula = values[0] ?? '';
13303
+ draft.formulaUseTextColor = Boolean(style.textColor);
13304
+ draft.formulaUseCellColor = Boolean(style.cellColor);
13305
+ draft.formulaTextColor = colorInputValue(style.textColor, DEFAULT_COLORS.comparisonText);
13306
+ draft.formulaCellColor = colorInputValue(style.cellColor, DEFAULT_COLORS.comparisonCell);
13307
+ }
13130
13308
  return draft;
13131
13309
  }
13132
13310
  function buildConditionalRule(draft) {
@@ -13167,6 +13345,34 @@ function buildConditionalRule(draft) {
13167
13345
  value
13168
13346
  ]
13169
13347
  };
13348
+ } else if ('formula' === draft.type) {
13349
+ const formula = draft.formula.trim();
13350
+ if (!formula) return {
13351
+ error: '请输入条件格式公式。'
13352
+ };
13353
+ const formulaSource = formula.replace(/^=/, '').trim();
13354
+ if (!formulaSource) return {
13355
+ error: '请输入条件格式公式,例如 =A2>0。'
13356
+ };
13357
+ if (Array.from(formulaSource).length > 255) return {
13358
+ error: '条件格式公式不能超过 255 个字符。'
13359
+ };
13360
+ if (!draft.formulaUseTextColor && !draft.formulaUseCellColor) return {
13361
+ error: '至少启用一种文字或填充颜色。'
13362
+ };
13363
+ rule = {
13364
+ type: 'default',
13365
+ cellrange,
13366
+ format: {
13367
+ textColor: draft.formulaUseTextColor ? draft.formulaTextColor : null,
13368
+ cellColor: draft.formulaUseCellColor ? draft.formulaCellColor : null
13369
+ },
13370
+ conditionName: 'formula',
13371
+ conditionRange: [],
13372
+ conditionValue: [
13373
+ `=${formulaSource}`
13374
+ ]
13375
+ };
13170
13376
  } else if ('dataBar' === draft.type) {
13171
13377
  const thresholds = savedVisualThresholds(draft.barThresholds);
13172
13378
  const minLength = Number(draft.barMinLength);
@@ -14417,7 +14623,7 @@ function formatSpreadsheetRowPageBreaks(breaks) {
14417
14623
  return normalizedBreaks(breaks).map((row)=>String(row + 1)).join(', ');
14418
14624
  }
14419
14625
  function parseSpreadsheetColumnPageBreaks(value, maximumColumn) {
14420
- return parsePageBreakList(value, maximumColumn, work_spreadsheet_page_breaks_decodeColumn);
14626
+ return parsePageBreakList(value, maximumColumn, decodeColumn);
14421
14627
  }
14422
14628
  function formatSpreadsheetColumnPageBreaks(breaks) {
14423
14629
  return normalizedBreaks(breaks).map(encodeColumn).join(', ');
@@ -14437,7 +14643,7 @@ function parsePageBreakList(value, maximum, decode) {
14437
14643
  function normalizedBreaks(breaks) {
14438
14644
  return Array.from(new Set((breaks ?? []).filter((value)=>Number.isSafeInteger(value) && value > 0))).sort((left, right)=>left - right);
14439
14645
  }
14440
- function work_spreadsheet_page_breaks_decodeColumn(token) {
14646
+ function decodeColumn(token) {
14441
14647
  const match = /^\$?([A-Z]+)$/i.exec(token);
14442
14648
  if (!match) return null;
14443
14649
  let value = 0;
@@ -21586,6 +21792,10 @@ function SpreadsheetConditionalFormatPanel({ content, onChange }) {
21586
21792
  value: 'icons',
21587
21793
  label: '图标集'
21588
21794
  },
21795
+ {
21796
+ value: 'formula',
21797
+ label: '自定义公式'
21798
+ },
21589
21799
  ...'toolbarRule' === draft.type ? [
21590
21800
  {
21591
21801
  value: 'toolbarRule',
@@ -21645,6 +21855,76 @@ function SpreadsheetConditionalFormatPanel({ content, onChange }) {
21645
21855
  ...draft,
21646
21856
  ...patch
21647
21857
  })
21858
+ }) : 'formula' === draft.type ? /*#__PURE__*/ jsxs(Fragment, {
21859
+ children: [
21860
+ /*#__PURE__*/ jsxs("div", {
21861
+ className: "work-office-field reference",
21862
+ children: [
21863
+ /*#__PURE__*/ jsx("span", {
21864
+ children: "公式"
21865
+ }),
21866
+ /*#__PURE__*/ jsx(office_text_field_OfficeTextField, {
21867
+ "aria-label": "条件格式公式",
21868
+ value: draft.formula,
21869
+ maxLength: 256,
21870
+ placeholder: "=A2>0",
21871
+ onChange: (event)=>setDraft({
21872
+ ...draft,
21873
+ formula: event.target.value
21874
+ })
21875
+ }),
21876
+ /*#__PURE__*/ jsxs("small", {
21877
+ children: [
21878
+ "以应用范围左上角为相对锚点;只读取本地缓存值。",
21879
+ ' ',
21880
+ Array.from(draft.formula.replace(/^=/, '')).length,
21881
+ "/",
21882
+ 255
21883
+ ]
21884
+ })
21885
+ ]
21886
+ }),
21887
+ /*#__PURE__*/ jsx("p", {
21888
+ className: "work-office-field-hint",
21889
+ children: "支持常用 Excel 函数、相对/绝对引用和有限区域;无法本地求值时保持原样。"
21890
+ }),
21891
+ /*#__PURE__*/ jsx(OfficeCheckbox, {
21892
+ className: "toggle",
21893
+ ariaLabel: "公式规则使用文字颜色",
21894
+ checked: draft.formulaUseTextColor,
21895
+ onCheckedChange: (formulaUseTextColor)=>setDraft({
21896
+ ...draft,
21897
+ formulaUseTextColor
21898
+ }),
21899
+ children: "使用文字颜色"
21900
+ }),
21901
+ draft.formulaUseTextColor && /*#__PURE__*/ jsx(ColorField, {
21902
+ label: "文字颜色",
21903
+ value: draft.formulaTextColor,
21904
+ onChange: (formulaTextColor)=>setDraft({
21905
+ ...draft,
21906
+ formulaTextColor
21907
+ })
21908
+ }),
21909
+ /*#__PURE__*/ jsx(OfficeCheckbox, {
21910
+ className: "toggle",
21911
+ ariaLabel: "公式规则使用填充颜色",
21912
+ checked: draft.formulaUseCellColor,
21913
+ onCheckedChange: (formulaUseCellColor)=>setDraft({
21914
+ ...draft,
21915
+ formulaUseCellColor
21916
+ }),
21917
+ children: "使用填充颜色"
21918
+ }),
21919
+ draft.formulaUseCellColor && /*#__PURE__*/ jsx(ColorField, {
21920
+ label: "填充颜色",
21921
+ value: draft.formulaCellColor,
21922
+ onChange: (formulaCellColor)=>setDraft({
21923
+ ...draft,
21924
+ formulaCellColor
21925
+ })
21926
+ })
21927
+ ]
21648
21928
  }) : 'colorGradation' === draft.type ? /*#__PURE__*/ jsxs(Fragment, {
21649
21929
  children: [
21650
21930
  /*#__PURE__*/ jsxs("div", {
@@ -26523,6 +26803,10 @@ function pruneSpreadsheetCollaborationViewState(view, content) {
26523
26803
  for (const sheetId of view.zoomBySheet.keys())if (!sheetIds.has(sheetId)) view.zoomBySheet.delete(sheetId);
26524
26804
  }
26525
26805
  const validationTypes = [
26806
+ {
26807
+ value: 'custom',
26808
+ label: '自定义公式'
26809
+ },
26526
26810
  {
26527
26811
  value: 'dropdown',
26528
26812
  label: '序列'
@@ -26679,7 +26963,7 @@ function SpreadsheetDataValidationDialog({ source, restoreFocusTarget, onApply,
26679
26963
  }, option.value))
26680
26964
  })
26681
26965
  }),
26682
- 'dropdown' !== value.type && /*#__PURE__*/ jsx(field_Field, {
26966
+ 'dropdown' !== value.type && 'custom' !== value.type && /*#__PURE__*/ jsx(field_Field, {
26683
26967
  label: "数据",
26684
26968
  children: /*#__PURE__*/ jsx("select", {
26685
26969
  value: value.type2,
@@ -26741,6 +27025,13 @@ function SpreadsheetDataValidationDialog({ source, restoreFocusTarget, onApply,
26741
27025
  setTouched(true);
26742
27026
  }
26743
27027
  })
27028
+ }) : 'custom' === value.type ? /*#__PURE__*/ jsx(SpreadsheetDataValidationCustomFormulaField, {
27029
+ value: value.value1,
27030
+ error: visibleError,
27031
+ onTouched: ()=>setTouched(true),
27032
+ onValueChange: (value1)=>update({
27033
+ value1
27034
+ })
26744
27035
  }) : /*#__PURE__*/ jsx(SpreadsheetDataValidationBoundaryFields, {
26745
27036
  type: value.type,
26746
27037
  type2: value.type2,
@@ -26900,6 +27191,45 @@ function SpreadsheetDataValidationDialog({ source, restoreFocusTarget, onApply,
26900
27191
  })
26901
27192
  });
26902
27193
  }
27194
+ function SpreadsheetDataValidationCustomFormulaField({ error, onTouched, onValueChange, value }) {
27195
+ return /*#__PURE__*/ jsxs("div", {
27196
+ className: "work-spreadsheet-data-validation-custom-formula",
27197
+ children: [
27198
+ /*#__PURE__*/ jsx(field_Field, {
27199
+ label: "公式",
27200
+ required: true,
27201
+ description: "公式必须返回 TRUE;相对引用以每个选定区域的左上角为基准。仅计算本地单元格和区域,不访问网络。",
27202
+ error: error ?? void 0,
27203
+ children: /*#__PURE__*/ jsx("textarea", {
27204
+ "aria-label": "公式",
27205
+ rows: 2,
27206
+ maxLength: 255,
27207
+ placeholder: '例如:=AND(A1<>"",A1<=100)',
27208
+ autoCapitalize: "none",
27209
+ spellCheck: false,
27210
+ value: value,
27211
+ onBlur: onTouched,
27212
+ onChange: (event)=>{
27213
+ onValueChange(event.currentTarget.value);
27214
+ onTouched();
27215
+ }
27216
+ })
27217
+ }),
27218
+ /*#__PURE__*/ jsxs("p", {
27219
+ className: "work-spreadsheet-data-validation-formula-note",
27220
+ children: [
27221
+ /*#__PURE__*/ jsx(Sigma, {
27222
+ size: 14,
27223
+ "aria-hidden": "true"
27224
+ }),
27225
+ /*#__PURE__*/ jsx("span", {
27226
+ children: "支持常用 Excel 函数、单元格引用和区域引用;无法安全求值时会阻止输入。"
27227
+ })
27228
+ ]
27229
+ })
27230
+ ]
27231
+ });
27232
+ }
26903
27233
  function SpreadsheetDataValidationBoundaryFields({ error, onTouched, onValueChange, type, type2, value1, value2 }) {
26904
27234
  const needsSecond = spreadsheetDataValidationNeedsSecondValue(type2);
26905
27235
  const inputMode = 'date' === type ? 'text' : 'decimal';
@@ -27122,6 +27452,68 @@ function cloneSpreadsheetDataValidationItem(item) {
27122
27452
  hintTitle: item.hintTitle
27123
27453
  };
27124
27454
  }
27455
+ const MAX_SPREADSHEET_CUSTOM_VALIDATION_FORMULA_LENGTH = 255;
27456
+ function evaluateSpreadsheetCustomValidation(content, sheetId, row, column, item, value) {
27457
+ const sheet = content.sheets.find((candidate)=>candidate.id === sheetId);
27458
+ if (!sheet) return unsupportedCustomValidation('找不到自定义公式所在的工作表。');
27459
+ const proposed = spreadsheetCustomValidationValue(value);
27460
+ const normalizedProposed = spreadsheetCustomValidationBlank(proposed) ? '' : proposed;
27461
+ if (false !== item.allowBlank && '' === normalizedProposed) return {
27462
+ supported: true,
27463
+ valid: true
27464
+ };
27465
+ const formula = item.value1.trim().replace(/^=/, '').trim();
27466
+ if (!formula) return unsupportedCustomValidation('自定义公式为空。');
27467
+ if (Array.from(formula).length > MAX_SPREADSHEET_CUSTOM_VALIDATION_FORMULA_LENGTH) return unsupportedCustomValidation('自定义公式超过 255 个字符。');
27468
+ const ranges = parseSpreadsheetCellRanges(item.rangeTxt);
27469
+ const anchor = spreadsheetCustomValidationAnchor(ranges, row, column);
27470
+ const result = evaluateSpreadsheetLocalFormula(formula, {
27471
+ sheets: content.sheets.flatMap((candidate)=>candidate.id ? [
27472
+ {
27473
+ ...candidate,
27474
+ id: candidate.id
27475
+ }
27476
+ ] : []),
27477
+ sheetId,
27478
+ row,
27479
+ column,
27480
+ anchorRow: anchor?.row,
27481
+ anchorColumn: anchor?.column,
27482
+ proposedValue: normalizedProposed,
27483
+ useProposedValue: true
27484
+ });
27485
+ if (!result.supported) return {
27486
+ supported: false,
27487
+ valid: false,
27488
+ message: result.message ? `自定义${result.message}` : result.message
27489
+ };
27490
+ return {
27491
+ supported: true,
27492
+ valid: spreadsheetLocalFormulaBoolean(result.value)
27493
+ };
27494
+ }
27495
+ function spreadsheetCustomValidationAnchor(ranges, row, column) {
27496
+ const range = ranges?.find((candidate)=>spreadsheetCellRangeContains(candidate, row, column));
27497
+ return {
27498
+ row: range?.row[0] ?? row,
27499
+ column: range?.column[0] ?? column
27500
+ };
27501
+ }
27502
+ function spreadsheetCustomValidationValue(value) {
27503
+ if (!value || 'object' != typeof value || Array.isArray(value)) return value;
27504
+ if ('v' in value) return value.v;
27505
+ return value;
27506
+ }
27507
+ function spreadsheetCustomValidationBlank(value) {
27508
+ return null == value || 'string' == typeof value && '' === value.trim();
27509
+ }
27510
+ function unsupportedCustomValidation(message) {
27511
+ return {
27512
+ supported: false,
27513
+ valid: false,
27514
+ message
27515
+ };
27516
+ }
27125
27517
  const spreadsheetCellFormatAttributes = [
27126
27518
  'bl',
27127
27519
  'it',
@@ -29990,6 +30382,27 @@ function SpreadsheetEditorSurface({ autoFocus = true, content, collaborationHist
29990
30382
  const requestSheetId = sheetId ?? activeSheetIdRef.current;
29991
30383
  if (!requestSheetId || !item || 'object' != typeof item || pendingDataValidationRef.current) return pendingDataValidationRef.current ? false : void 0;
29992
30384
  const validationItem = item;
30385
+ if ('custom' === validationItem.type) {
30386
+ const evaluation = evaluateSpreadsheetCustomValidation(contentRef.current, requestSheetId, row, column, validationItem, value);
30387
+ if (evaluation.supported && evaluation.valid) return true;
30388
+ const displayItem = cloneSpreadsheetDataValidationItem(validationItem);
30389
+ if (!displayItem.errorMessage?.trim() && evaluation.message) displayItem.errorMessage = evaluation.message;
30390
+ const style = spreadsheetDataValidationErrorStyle(displayItem);
30391
+ const request = {
30392
+ column,
30393
+ failureText: evaluation.message ?? failureText,
30394
+ interaction: spreadsheetDataValidationInteraction(style),
30395
+ item: displayItem,
30396
+ row,
30397
+ sheetId: requestSheetId,
30398
+ value
30399
+ };
30400
+ pendingDataValidationRef.current = request;
30401
+ queueMicrotask(()=>{
30402
+ if (pendingDataValidationRef.current === request) setPendingDataValidation(request);
30403
+ });
30404
+ return false;
30405
+ }
29993
30406
  const style = spreadsheetDataValidationErrorStyle(validationItem);
29994
30407
  const request = {
29995
30408
  column,
@@ -30020,7 +30433,7 @@ function SpreadsheetEditorSurface({ autoFocus = true, content, collaborationHist
30020
30433
  const conditionalStylesBySheet = useMemo(()=>new Map(materializedContent.sheets.flatMap((sheet)=>sheet.id ? [
30021
30434
  [
30022
30435
  sheet.id,
30023
- spreadsheetConditionalFormatStyles(sheet)
30436
+ spreadsheetConditionalFormatStyles(sheet, materializedContent.sheets)
30024
30437
  ]
30025
30438
  ] : [])), [
30026
30439
  materializedContent.sheets
package/dist/4121.js CHANGED
@@ -2902,9 +2902,16 @@ const WORK_TEMPLATES = [
2902
2902
  id: 'data-validation',
2903
2903
  kind: 'spreadsheet',
2904
2904
  name: '数据验证',
2905
- description: '下拉列表、输入提示与错误警告',
2905
+ description: '自定义公式、下拉列表、输入提示与错误警告',
2906
2906
  accent: '#13795b'
2907
2907
  },
2908
+ {
2909
+ id: 'conditional-format',
2910
+ kind: 'spreadsheet',
2911
+ name: '公式条件格式',
2912
+ description: '相对引用、跨表阈值、停止后续规则与本地实时强调',
2913
+ accent: '#2f6fed'
2914
+ },
2908
2915
  {
2909
2916
  id: 'structured-references',
2910
2917
  kind: 'spreadsheet',
@@ -2965,6 +2972,7 @@ function initialTitle(templateId, kind) {
2965
2972
  'proofing-languages': '校对语言示例',
2966
2973
  'quarterly-plan': '季度执行计划',
2967
2974
  'data-validation': '数据验证示例',
2975
+ 'conditional-format': '公式条件格式示例',
2968
2976
  'structured-references': '结构化引用示例',
2969
2977
  'strategy-deck': '业务策略汇报',
2970
2978
  'animated-deck': '进入与退出动画示例'
@@ -3265,6 +3273,10 @@ function contentForTemplate(templateId) {
3265
3273
  type: 'spreadsheet',
3266
3274
  sheets: dataValidationTemplateSheets()
3267
3275
  };
3276
+ if ('conditional-format' === templateId) return {
3277
+ type: 'spreadsheet',
3278
+ sheets: conditionalFormatTemplateSheets()
3279
+ };
3268
3280
  if ('structured-references' === templateId) return {
3269
3281
  type: 'spreadsheet',
3270
3282
  sheets: structuredReferenceTemplateSheets()
@@ -3566,6 +3578,32 @@ function dataValidationTemplateSheets() {
3566
3578
  hintTitle: 'Priority',
3567
3579
  hintValue: '1 is highest priority; 5 is lowest.'
3568
3580
  })
3581
+ },
3582
+ {
3583
+ ranges: [
3584
+ {
3585
+ row: [
3586
+ 1,
3587
+ 5
3588
+ ],
3589
+ column: [
3590
+ 4,
3591
+ 4
3592
+ ]
3593
+ }
3594
+ ],
3595
+ item: dataValidationTemplateItem({
3596
+ type: 'custom',
3597
+ rangeTxt: 'E2:E6',
3598
+ value1: 'LEN(E2)>0',
3599
+ allowBlank: false,
3600
+ errorStyle: 'stop',
3601
+ errorTitle: 'Owner required',
3602
+ errorMessage: 'Enter an owner for every task.',
3603
+ hintShow: true,
3604
+ hintTitle: 'Owner',
3605
+ hintValue: 'The custom formula checks that this cell is not blank.'
3606
+ })
3569
3607
  }
3570
3608
  ],
3571
3609
  luckysheet_select_save: [
@@ -3621,6 +3659,247 @@ function dataValidationTemplateSheets() {
3621
3659
  }
3622
3660
  ];
3623
3661
  }
3662
+ function conditionalFormatTemplateSheets() {
3663
+ const status = emptyMatrix(18, 8);
3664
+ status[0][0] = styledCell('公式条件格式 · 发布看板', {
3665
+ bl: 1,
3666
+ fs: 16,
3667
+ fc: '#ffffff',
3668
+ bg: '#2f6fed'
3669
+ });
3670
+ status[1][0] = styledCell('公式以 A2:H7 左上角为锚点;先标记阻塞项,再强调达到阈值的任务。', {
3671
+ fc: '#49617f',
3672
+ fs: 10
3673
+ });
3674
+ [
3675
+ '任务',
3676
+ '负责人',
3677
+ '完成率',
3678
+ '状态',
3679
+ '预算',
3680
+ '实际',
3681
+ '偏差',
3682
+ '备注'
3683
+ ].forEach((value, column)=>{
3684
+ status[2][column] = headerCell(value);
3685
+ });
3686
+ const rows = [
3687
+ [
3688
+ '需求确认',
3689
+ 'Avery',
3690
+ 1,
3691
+ '完成',
3692
+ 12000,
3693
+ 11800,
3694
+ '按期',
3695
+ '可发布'
3696
+ ],
3697
+ [
3698
+ '接口联调',
3699
+ 'Morgan',
3700
+ 0.82,
3701
+ '进行中',
3702
+ 18000,
3703
+ 20100,
3704
+ '超支',
3705
+ '关注预算'
3706
+ ],
3707
+ [
3708
+ '回归测试',
3709
+ 'Riley',
3710
+ 0.54,
3711
+ '阻塞',
3712
+ 15000,
3713
+ 7600,
3714
+ '暂停',
3715
+ '等待依赖'
3716
+ ],
3717
+ [
3718
+ '文档发布',
3719
+ 'Jordan',
3720
+ 0.76,
3721
+ '进行中',
3722
+ 9000,
3723
+ 8800,
3724
+ '按期',
3725
+ '待审阅'
3726
+ ],
3727
+ [
3728
+ '版本发布',
3729
+ 'Taylor',
3730
+ 0.38,
3731
+ '未开始',
3732
+ 22000,
3733
+ 0,
3734
+ '未开始',
3735
+ '尚未排期'
3736
+ ],
3737
+ [
3738
+ '复盘会议',
3739
+ 'Avery',
3740
+ 0.9,
3741
+ '进行中',
3742
+ 4000,
3743
+ 3500,
3744
+ '按期',
3745
+ '本周完成'
3746
+ ]
3747
+ ];
3748
+ rows.forEach((row, rowIndex)=>{
3749
+ row.forEach((value, columnIndex)=>{
3750
+ status[rowIndex + 3][columnIndex] = styledCell(value, {
3751
+ bg: rowIndex % 2 ? '#f6f9ff' : '#ffffff',
3752
+ ...2 === columnIndex ? {
3753
+ ct: {
3754
+ fa: '0%',
3755
+ t: 'n'
3756
+ },
3757
+ m: `${Math.round(100 * Number(value))}%`
3758
+ } : {},
3759
+ ...4 === columnIndex || 5 === columnIndex ? {
3760
+ ct: {
3761
+ fa: '#,##0',
3762
+ t: 'n'
3763
+ },
3764
+ m: Number(value).toLocaleString()
3765
+ } : {}
3766
+ });
3767
+ });
3768
+ });
3769
+ status[11][0] = styledCell('本地规则示例', {
3770
+ bl: 1,
3771
+ fc: '#244b9b'
3772
+ });
3773
+ status[12][0] = styledCell('阻塞项(停止后续规则)');
3774
+ status[12][1] = styledCell('=$D4="阻塞"');
3775
+ status[13][0] = styledCell('达标项(跨表阈值)');
3776
+ status[13][1] = styledCell('=AND($C4>=Limits!$A$2,$D4<>"阻塞")');
3777
+ const blockRule = {
3778
+ type: 'default',
3779
+ cellrange: [
3780
+ {
3781
+ row: [
3782
+ 3,
3783
+ 8
3784
+ ],
3785
+ column: [
3786
+ 0,
3787
+ 7
3788
+ ]
3789
+ }
3790
+ ],
3791
+ format: {
3792
+ textColor: '#9c0006',
3793
+ cellColor: '#ffc7ce'
3794
+ },
3795
+ conditionName: 'formula',
3796
+ conditionRange: [],
3797
+ conditionValue: [
3798
+ '=$D4="阻塞"'
3799
+ ],
3800
+ stopIfTrue: true
3801
+ };
3802
+ const completedRule = {
3803
+ type: 'default',
3804
+ cellrange: [
3805
+ {
3806
+ row: [
3807
+ 3,
3808
+ 8
3809
+ ],
3810
+ column: [
3811
+ 0,
3812
+ 7
3813
+ ]
3814
+ }
3815
+ ],
3816
+ format: {
3817
+ textColor: '#006100',
3818
+ cellColor: '#c6efce'
3819
+ },
3820
+ conditionName: 'formula',
3821
+ conditionRange: [],
3822
+ conditionValue: [
3823
+ '=AND($C4>=Limits!$A$2,$D4<>"阻塞")'
3824
+ ]
3825
+ };
3826
+ return [
3827
+ {
3828
+ id: createWorkId('sheet'),
3829
+ name: 'Status',
3830
+ status: 1,
3831
+ order: 0,
3832
+ row: 18,
3833
+ column: 8,
3834
+ data: status,
3835
+ luckysheet_conditionformat_save: [
3836
+ blockRule,
3837
+ completedRule
3838
+ ],
3839
+ config: {
3840
+ columnlen: {
3841
+ 0: 132,
3842
+ 1: 92,
3843
+ 2: 84,
3844
+ 3: 86,
3845
+ 4: 92,
3846
+ 5: 92,
3847
+ 6: 84,
3848
+ 7: 116
3849
+ },
3850
+ rowlen: {
3851
+ 0: 34,
3852
+ 1: 26,
3853
+ 2: 28,
3854
+ 12: 24,
3855
+ 13: 24
3856
+ },
3857
+ merge: {
3858
+ '0_0': {
3859
+ r: 0,
3860
+ c: 0,
3861
+ rs: 1,
3862
+ cs: 8
3863
+ }
3864
+ }
3865
+ }
3866
+ },
3867
+ {
3868
+ id: createWorkId('sheet'),
3869
+ name: 'Limits',
3870
+ status: 0,
3871
+ order: 1,
3872
+ row: 8,
3873
+ column: 4,
3874
+ data: [
3875
+ [
3876
+ headerCell('完成率阈值'),
3877
+ styledCell('说明')
3878
+ ],
3879
+ [
3880
+ styledCell(0.8, {
3881
+ ct: {
3882
+ fa: '0%',
3883
+ t: 'n'
3884
+ },
3885
+ m: '80%'
3886
+ }),
3887
+ styledCell('达到阈值并且未阻塞的任务使用绿色强调。')
3888
+ ]
3889
+ ],
3890
+ config: {
3891
+ columnlen: {
3892
+ 0: 120,
3893
+ 1: 300
3894
+ },
3895
+ rowlen: {
3896
+ 0: 28,
3897
+ 1: 36
3898
+ }
3899
+ }
3900
+ }
3901
+ ];
3902
+ }
3624
3903
  function structuredReferenceTemplateSheets() {
3625
3904
  const sales = emptyMatrix(16, 10);
3626
3905
  sales[0][0] = styledCell('Sales · 结构化引用', {
package/dist/core.js CHANGED
@@ -3325,6 +3325,7 @@ function parseListFormula(value) {
3325
3325
  }
3326
3326
  function fortuneValidationType(value) {
3327
3327
  const types = {
3328
+ custom: 'custom',
3328
3329
  list: 'dropdown',
3329
3330
  whole: 'number_integer',
3330
3331
  decimal: 'number',
@@ -3335,6 +3336,7 @@ function fortuneValidationType(value) {
3335
3336
  }
3336
3337
  function xlsxValidationType(value) {
3337
3338
  const types = {
3339
+ custom: 'custom',
3338
3340
  dropdown: 'list',
3339
3341
  number: 'decimal',
3340
3342
  number_integer: 'whole',
@@ -15,7 +15,7 @@ export declare const SPREADSHEET_CONDITIONAL_COMPARISONS: {
15
15
  }[];
16
16
  export interface ConditionalRuleDraft {
17
17
  sheetId: string;
18
- type: 'cellComparison' | 'colorGradation' | 'dataBar' | 'icons' | 'toolbarRule';
18
+ type: 'cellComparison' | 'colorGradation' | 'dataBar' | 'icons' | 'formula' | 'toolbarRule';
19
19
  reference: string;
20
20
  stopIfTrue: boolean;
21
21
  preservedRule: FortuneConditionalFormatRule | null;
@@ -26,6 +26,11 @@ export interface ConditionalRuleDraft {
26
26
  comparisonCellColor: string;
27
27
  comparisonUseTextColor: boolean;
28
28
  comparisonUseCellColor: boolean;
29
+ formula: string;
30
+ formulaTextColor: string;
31
+ formulaCellColor: string;
32
+ formulaUseTextColor: boolean;
33
+ formulaUseCellColor: boolean;
29
34
  scaleSize: '2' | '3';
30
35
  minimumColor: string;
31
36
  midpointColor: string;
@@ -0,0 +1,18 @@
1
+ import type { WorkSpreadsheetContent, WorkSpreadsheetDataValidationItem } from '../work-types';
2
+ /** OOXML limits a data-validation formula to 255 characters. */
3
+ export declare const MAX_SPREADSHEET_CUSTOM_VALIDATION_FORMULA_LENGTH = 255;
4
+ /** Keep synchronous browser validation bounded even for a hostile workbook. */
5
+ export declare const MAX_SPREADSHEET_CUSTOM_VALIDATION_REFERENCED_CELLS = 1024;
6
+ export interface SpreadsheetCustomValidationResult {
7
+ message?: string;
8
+ supported: boolean;
9
+ valid: boolean;
10
+ }
11
+ /**
12
+ * Evaluates a local Excel custom-validation formula synchronously.
13
+ *
14
+ * The editor deliberately evaluates against cached cell values. Formula
15
+ * recalculation remains a separate kernel concern; a formula cell without a
16
+ * cached value therefore fails closed instead of silently accepting input.
17
+ */
18
+ export declare function evaluateSpreadsheetCustomValidation(content: WorkSpreadsheetContent, sheetId: string, row: number, column: number, item: WorkSpreadsheetDataValidationItem, value: unknown): SpreadsheetCustomValidationResult;
@@ -1,7 +1,7 @@
1
1
  import type { WorkSpreadsheetContent, WorkSpreadsheetDataValidationErrorStyle, WorkSpreadsheetDataValidationItem, WorkSpreadsheetSheet } from '../work-types';
2
2
  import { type SpreadsheetCellRange } from './spreadsheet-cell-range';
3
3
  export declare const MAX_SPREADSHEET_DATA_VALIDATION_CELLS = 10000;
4
- export type SpreadsheetDataValidationType = 'date' | 'dropdown' | 'number' | 'number_integer' | 'text_length';
4
+ export type SpreadsheetDataValidationType = 'custom' | 'date' | 'dropdown' | 'number' | 'number_integer' | 'text_length';
5
5
  export type SpreadsheetDataValidationOperator = 'between' | 'equal' | 'greaterOrEqualTo' | 'lessThan' | 'lessThanOrEqualTo' | 'moreThanThe' | 'noEarlierThan' | 'noLaterThan' | 'notBetween' | 'notEqualTo' | 'earlierThan' | 'laterThan';
6
6
  export interface SpreadsheetDataValidationDialogValue {
7
7
  allowBlank: boolean;
@@ -46,7 +46,7 @@ export type SpreadsheetDataValidationResult = {
46
46
  message: string;
47
47
  ok: false;
48
48
  };
49
- export type SpreadsheetDataValidationErrorCode = 'invalid-date' | 'invalid-list-source' | 'invalid-number' | 'invalid-operator' | 'invalid-range' | 'invalid-text-length' | 'missing-value' | 'multiple-list-columns' | 'out-of-bounds' | 'protected-range' | 'range-too-large' | 'sheet-not-found' | 'value-order';
49
+ export type SpreadsheetDataValidationErrorCode = 'invalid-custom-formula' | 'invalid-date' | 'invalid-list-source' | 'invalid-number' | 'invalid-operator' | 'invalid-range' | 'invalid-text-length' | 'missing-value' | 'multiple-list-columns' | 'out-of-bounds' | 'protected-range' | 'range-too-large' | 'sheet-not-found' | 'value-order';
50
50
  export declare function spreadsheetDataValidationOperators(type: SpreadsheetDataValidationType): readonly SpreadsheetDataValidationOperator[];
51
51
  export declare function createSpreadsheetDataValidationDialogSource(content: WorkSpreadsheetContent, target: SpreadsheetDataValidationTarget): SpreadsheetDataValidationDialogSource | null;
52
52
  export declare function validateSpreadsheetDataValidationRequest(content: WorkSpreadsheetContent, request: SpreadsheetDataValidationRequest): SpreadsheetDataValidationResult;
@@ -1,10 +1,12 @@
1
1
  import type { Sheet } from '@fortune-sheet/core';
2
2
  import { type SpreadsheetConditionalDataBar } from './work-spreadsheet-conditional-data-bar';
3
3
  import { type SpreadsheetConditionalIcon } from './work-spreadsheet-conditional-icons';
4
+ /** Full formula-rule scans stay bounded; larger ranges use materialized cells. */
5
+ export declare const MAX_SPREADSHEET_CONDITIONAL_FORMULA_SCAN_CELLS = 10000;
4
6
  export interface SpreadsheetConditionalCellStyle {
5
7
  textColor?: string;
6
8
  cellColor?: string;
7
9
  dataBar?: SpreadsheetConditionalDataBar;
8
10
  icon?: SpreadsheetConditionalIcon;
9
11
  }
10
- export declare function spreadsheetConditionalFormatStyles(sheet: Sheet): Map<string, SpreadsheetConditionalCellStyle>;
12
+ export declare function spreadsheetConditionalFormatStyles(sheet: Sheet, formulaSheets?: readonly Sheet[]): Map<string, SpreadsheetConditionalCellStyle>;
@@ -2,6 +2,7 @@ import type { WorkSpreadsheetDataValidationErrorStyle } from './work-types';
2
2
  export declare const SPREADSHEET_DATA_VALIDATION_TITLE_LIMIT = 32;
3
3
  export declare const SPREADSHEET_DATA_VALIDATION_HINT_LIMIT = 255;
4
4
  export declare const SPREADSHEET_DATA_VALIDATION_ERROR_LIMIT = 225;
5
+ export declare const SPREADSHEET_DATA_VALIDATION_FORMULA_LIMIT = 255;
5
6
  export declare const SPREADSHEET_DATA_VALIDATION_ERROR_STYLES: readonly ["stop", "warning", "information"];
6
7
  export declare function normalizeSpreadsheetDataValidationErrorStyle(value: unknown): WorkSpreadsheetDataValidationErrorStyle;
7
8
  export declare function boundedSpreadsheetDataValidationText(value: unknown, limit: number): string;
@@ -0,0 +1,43 @@
1
+ import type { CellMatrix } from '@fortune-sheet/core';
2
+ /** OOXML and the browser editor keep local rule formulas deliberately small. */
3
+ export declare const MAX_SPREADSHEET_LOCAL_FORMULA_LENGTH = 255;
4
+ /** Keep synchronous browser formula evaluation bounded for hostile workbooks. */
5
+ export declare const MAX_SPREADSHEET_LOCAL_FORMULA_REFERENCED_CELLS = 1024;
6
+ export interface SpreadsheetFormulaSheet {
7
+ id: string;
8
+ name: string;
9
+ data?: CellMatrix;
10
+ celldata?: readonly SpreadsheetFormulaCell[];
11
+ }
12
+ export interface SpreadsheetFormulaCell {
13
+ r: number;
14
+ c: number;
15
+ v: unknown;
16
+ }
17
+ export interface SpreadsheetLocalFormulaOptions {
18
+ sheets: readonly SpreadsheetFormulaSheet[];
19
+ sheetId: string;
20
+ row: number;
21
+ column: number;
22
+ /** The first row of the rule/validation range. Defaults to the target row. */
23
+ anchorRow?: number;
24
+ /** The first column of the rule/validation range. Defaults to the target column. */
25
+ anchorColumn?: number;
26
+ /** Substitute the target cell with this value, as data validation does. */
27
+ proposedValue?: unknown;
28
+ useProposedValue?: boolean;
29
+ }
30
+ export interface SpreadsheetLocalFormulaResult {
31
+ supported: boolean;
32
+ value?: unknown;
33
+ message?: string;
34
+ }
35
+ /**
36
+ * Evaluate a bounded Excel formula against the workbook's cached local data.
37
+ *
38
+ * This is intentionally shared by custom data validation and conditional
39
+ * formatting. It never recalculates formula cells or reaches a remote source;
40
+ * an unavailable dependency therefore fails closed.
41
+ */
42
+ export declare function evaluateSpreadsheetLocalFormula(formula: string, options: SpreadsheetLocalFormulaOptions): SpreadsheetLocalFormulaResult;
43
+ export declare function spreadsheetLocalFormulaBoolean(value: unknown): boolean;
Binary file
package/dist/styles.css CHANGED
@@ -17939,6 +17939,27 @@ sub [data-office-character-position-half-points], sup [data-office-character-pos
17939
17939
  display: grid;
17940
17940
  }
17941
17941
 
17942
+ .work-spreadsheet-data-validation-custom-formula {
17943
+ gap: 8px;
17944
+ display: grid;
17945
+ }
17946
+
17947
+ .work-spreadsheet-data-validation-formula-note {
17948
+ color: var(--a3s-muted);
17949
+ align-items: flex-start;
17950
+ gap: 6px;
17951
+ margin: -2px 0 0;
17952
+ font-size: 10px;
17953
+ line-height: 1.45;
17954
+ display: flex;
17955
+ }
17956
+
17957
+ .work-spreadsheet-data-validation-formula-note svg {
17958
+ color: var(--work-spreadsheet-accent, #159469);
17959
+ flex: none;
17960
+ margin-top: 1px;
17961
+ }
17962
+
17942
17963
  .work-spreadsheet-data-validation-inline-error {
17943
17964
  color: var(--a3s-danger, #c23535);
17944
17965
  grid-column: 1 / -1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a3s-lab/office",
3
- "version": "0.41.0",
3
+ "version": "0.43.0",
4
4
  "description": "Open-source collaborative browser editors for documents, Markdown, spreadsheets, presentations, and PDFs.",
5
5
  "keywords": [
6
6
  "office",
@@ -106,6 +106,7 @@
106
106
  "playground:visual:document-opentype": "playwright test visual-tests/document-opentype.functional.spec.ts --config playwright.config.ts",
107
107
  "playground:visual:document-strike": "playwright test visual-tests/document-strike.functional.spec.ts --config playwright.config.ts",
108
108
  "playground:visual:spreadsheet-data-validation": "playwright test visual-tests/spreadsheet-data-validation.functional.spec.ts --config playwright.config.ts",
109
+ "playground:visual:spreadsheet-conditional-format": "playwright test visual-tests/spreadsheet-conditional-format.functional.spec.ts --config playwright.config.ts",
109
110
  "playground:visual:spreadsheet-date-time": "playwright test visual-tests/spreadsheet-date-time.functional.spec.ts --config playwright.config.ts",
110
111
  "playground:visual:spreadsheet-copy-from-above": "playwright test visual-tests/spreadsheet-copy-from-above.functional.spec.ts --config playwright.config.ts",
111
112
  "playground:visual:spreadsheet-custom-sort": "playwright test visual-tests/spreadsheet-custom-sort.functional.spec.ts --config playwright.config.ts",
@@ -224,6 +225,8 @@
224
225
  "test:e2e:spreadsheet-go-to:check": "a3s-test check tests/e2e/spreadsheet-go-to.acl --json",
225
226
  "test:e2e:spreadsheet-data-validation": "A3S_TEST_SUITE=tests/e2e/spreadsheet-data-validation.acl bash scripts/run-a3s-test-web-gate.sh",
226
227
  "test:e2e:spreadsheet-data-validation:check": "a3s-test check tests/e2e/spreadsheet-data-validation.acl --json",
228
+ "test:e2e:spreadsheet-conditional-format": "A3S_TEST_SUITE=tests/e2e/spreadsheet-conditional-format.acl bash scripts/run-a3s-test-web-gate.sh",
229
+ "test:e2e:spreadsheet-conditional-format:check": "a3s-test check tests/e2e/spreadsheet-conditional-format.acl --json",
227
230
  "test:e2e:spreadsheet-diagonal-borders": "a3s-test run tests/e2e/spreadsheet-diagonal-borders.acl --json",
228
231
  "test:e2e:spreadsheet-diagonal-borders:check": "a3s-test check tests/e2e/spreadsheet-diagonal-borders.acl --json",
229
232
  "test:e2e:spreadsheet-date-time": "a3s-test run tests/e2e/spreadsheet-date-time.acl --json",