Insert > Line or Area and the All Charts list offer 100% Stacked Area,
and insert.chart and chart.set take it as type area with subtype
stacked100. Each series' area stacks on the ones before it as its
share of the category's total. When every series on the value axis is
100% stacked and no value is negative, the axis runs from 0% to 100%.
A blank cell counts as zero, and a category whose total is zero stays
on the baseline.
An XLSX area chart with percentStacked grouping now opens as 100%
stacked and is saved back as percentStacked. It used to open as a
stacked area and was saved as stacked.
The submenu lists up to 100 cells. When the list was taller than the
window, the cells below its bottom edge could not be reached. The list
now sits in a scroll area 400 points high.
The status bar named the first circular reference in the list, even
when it was on another sheet. It now names the first one on the active
sheet, and shows only Circular References when every loop is on other
sheets, as Excel's status bar does.
formulas.errorChecking listed each circular reference as Sheet!A1 with
the sheet name as it is, so a sheet named Bob's Q1 gave Bob's Q1!B2,
which a formula does not accept as a reference. The list now quotes the
name with quote_sheet, as references are written elsewhere in the
engine ('Bob''s Q1'!B2), and the status bar's Circular References
label uses the same form.
Formulas > Error Checking is now a split button. The button opens the
Error Checking dialog as before, and its arrow opens a menu with Error
Checking… and Circular References. Circular References lists a cell on
each circular reference as Sheet!A1, up to 100, and picking one selects
it on its own sheet and scrolls the grid to it. With no circular
reference the item is unavailable.
The calculation kept only the loops its latest run found, so typing in
any cell outside a loop emptied the list, and the status bar's Circular
References with it, while the loop was still there. A partial
recalculation now keeps the loops among the formulas it does not
evaluate. In automatic calculation a loop leaves the list once an edit
breaks it. In manual calculation only the edited formulas run, so a
listed cell stays until it is edited or the workbook is recalculated.
A full recalculation or iterative calculation builds the list again.
The Header & Footer tab now has the Scale with Document and Align with
Page Margins check boxes, and pageLayout.headerFooter takes
scaleWithDoc and alignWithMargins. XLSX already kept both settings,
but nothing else read them.
With Scale with Document on, header and footer text prints at the
sheet's print scale. With it off, the text keeps its point size. With
Align with Page Margins on, the left and right sections line up with
the page's side margins. With it off, they start 0.7 inch from the
paper's edges, or a quarter of the page width on paper narrower than
2.8 inches. The Page Layout view draws headers and footers and takes
clicks on them the same way. Both settings are on by default, so a
sheet printed at a scale other than 100% now prints its header and
footer text at that scale.
The Page Layout view row in docs/ui-parity.md listed both options as
missing. The Header & Footer tab has had both check boxes since #344, and
the first page and the even pages print and show their own header and
footer. The row now says so and lists Scale with Document and Align with
Page Margins as missing instead: XLSX keeps both settings, but no control
shows them and printing does not read them.
Data > Outline > Settings now has Summary rows below detail and Summary
columns to right of detail. With one cleared, each group's - / + button
sits on the line before the group, its bracket runs down to the last
detail line, and Hide Detail, Show Detail, the level buttons and
Ungroup mark or clear the collapsed flag on that line. Changing a
direction moves the flags of collapsed groups to their new summary
lines.
- model: Sheet::summary_below and summary_right (on by default, also
for older JSON).
- xlsx: read and written as sheetPr/outlinePr summaryBelow and
summaryRight; files that set them no longer lose them on save.
- engine: data.outlineSettings {sheet?, summaryBelow?, summaryRight?},
undoable; outline::groups_with places the summary line.
- ui: a Settings button in the Data tab's Outline group opens the
dialog; the outline band follows the setting.
The cell menu checked whether to offer Show Details by computing the
whole report again for each new active cell. pivot::detail_cells now
returns every value, subtotal and grand total cell of a report from one
computation, and the menu caches that set per document, revision, sheet
and PivotTable.
With Enable show details off, Show Details on a value cell of a report whose
source has changed gave the out-of-date error. It now gives the turned-off
message, and a label still reports that it is not a value cell. The message
names pivot.options with enableShowDetails set to true, since there is no
PivotTable Options dialog.
The sheet tabs took up to 60% of the tab bar and the horizontal
scrollbar the rest, and nothing between them could be dragged. The
gap at the scrollbar's left edge is now a handle: it shows the resize
cursor, and dragging it right gives the tabs more of the bar, until the
scrollbar is 100 px wide. After the window is narrowed, the scrollbar
is still 100 px wide. Dragging it left gives the scrollbar more, down
to the New sheet button. Room the tabs do not fill stays empty, and
New sheet stays after the last tab.
Each open workbook keeps its own share while it is open. The share is
not saved in the file, and a workbook starts at the default.
The cell menu of a PivotTable's value, subtotal or grand total cell now
has a Show Details item after Sort Z to A. It runs pivot.showDetails on
the active cell, which lists the cell's source records on a new sheet,
as double-clicking the cell does. Labels and headers of a current
report, and cells outside a report, do not offer it. In a report that
is out of date every cell of the report offers the item, and running it
shows the message that asks for a refresh.
Whether the menu offers the item is cached per document, revision,
sheet and active cell.
A PivotTable has an Enable show details option, set with the new
pivot.options command ({pivot?, enableShowDetails?: bool}). While it is
off, pivot.showDetails and double-clicking a value cell list no records:
the double-click shows a message and does not open the cell for editing.
Labels and headers still edit. Changing the option is one undo step and
does not refresh the report. pivot.fields reports the setting.
XLSX files keep it: an off option is written as enableDrill="0" on the
pivotTableDefinition, and enableDrill is read on open (on when absent).
When a fill starts from a date, the right-drag fill menu and the Auto
Fill Options button now also offer Fill Days, Fill Weekdays, Fill Months
and Fill Years. edit.autoFill takes them as the modes days, weekdays,
months and years.
A lone date steps by one unit. Two or more dates step by the difference
between the first two in that unit, or by one unit when both fall in
the same one. Fill Weekdays skips Saturday and Sunday, and a start on a
weekend moves to the next weekday first. Fill Months and Fill Years keep
the day of the month, clamped to the month's last day. A source that is
not all dates fills as a plain drag does.
Fills by months or years now keep the time of day of a date with a
time. A plain drag of dates a month apart and Series by Month or Year
used to drop it.
Agile decryption checked only the password verifier, which authenticates
the key — not the package. An attacker who could replace EncryptedPackage
ciphertext could change workbook content undetected ([MS-OFFCRYPTO]
2.3.4.14, the descriptor's dataIntegrity element).
The reader now requires the dataIntegrity pair, decrypts it with the
recovered package key, and checks HMAC(hash, hmacKey) over the whole
EncryptedPackage stream (StreamSize field included) against the stored
value before any plaintext is returned. Excel always writes the element
("SHOULD" in the spec); descriptors without it are refused as damaged
rather than accepted unauthenticated.
- crypto: RFC 2104/4231 HMAC test vectors, RFC-test cases for a key
longer than the block size
- fixtures write the pair like Excel; new tests: missing/empty/ragged
integrity fields, tampered ciphertext and descriptor, a changed
StreamSize field
save_fails_upfront_when_the_temp_name_is_taken,
save_keeps_the_destination_access_restrictions and
open_refuses_directories_and_unknowable_lengths in crates/engine/src/io.rs
all worked in temp_dir()/gridcraft-fs-safety and removed it at the start
and the end. cargo test runs them on parallel threads, so one test could
delete the folder while another was writing to it, and the other test
failed. Each test now uses its own folder, named with a short label and
the process id, as the gridcraft-write and gridcraft-sandbox tests in the
same file do.
Ctrl+Shift+U ran the Ctrl+U shortcut on Windows and Linux and underlined
the selection. In Excel it expands or collapses the formula bar, on
Windows and as Control+Shift+U on a Mac. It now does that on the grid
and in the cell editor or formula bar while editing, and the entry stays
open.
The new UI command view.expandFormulaBar {on?} sets the state, or
toggles it without a parameter, and returns {on}. The chevron at the end
of the formula bar is unchanged.
- floating_pictures: draw without retaining when eviction can't free
enough room, so a large workbook can't pin the cache past its budget
- xtask stats: skip unreadable subtrees instead of aborting the walk
- import-cert.sh: per-run private temp dir instead of predictable
paths in a shared root
- gridcraft-web: gate clipboard ops behind user activation
- chart raster: cap dash rendering to the stroked budget
- cli check: mark directories visited by identity on unix so hard-link
loops can't recurse; reuse one decompression of a reused part
- scripts: refuse `window.*` steps like file and link steps — activating
another window repointed every later step at a different open document
(finding 17).
- format: on a protected sheet, formatting may not change a cell's
protection flags: the format-cells permission can't drop `locked` and
then authorize content edits the password gates (finding 20).
- sheet.moveColumns: the insert/delete protection checks now carry the
command's own `sheet` parameter, so the sheet modified is the sheet
authorized, not whichever is active (finding 24).
Tests: unlock-via-format refused, cross-sheet move refused, window switch
refused, plus the positive controls. Engine: 462 tests pass; clippy clean.
- read_file: verify the opened file is regular and cap the bytes actually
taken at the 2 GB limit — a workbook-named symlink to an endless device
reported length 0 but streamed forever (finding 22).
- write_file: create the temporary file exclusively, so a planted file or
symlink at the predictable `book.xlsx.sctmp` path makes the save fail
instead of truncating the link's target (finding 25).
- write_file: inherit the destination's permissions, so saving an
owner-only workbook doesn't become world-readable through the rename
(finding 15); new files note restrictive defaults.
Tests: exclusive temp creation vs planted symlink, permissions preserved,
directory refused on open. Engine: 460 tests pass; clippy clean.
- clip_html: cap wildcard/glob lookback to a 12-byte window instead of scanning
outgoing text arbitrarily far for a semicolon (finding 18).
- print: wrap_lines binary-searches its break instead of rescanning the line
from the start for every band (finding 21).
- autofit: per-row merge index instead of scanning every merge per row during
autofit (finding 27) — behaviour covered by the existing autofit merge test.
- Save as Web Page: the used rectangle bounds the HTML (finding 28, #2387) —
a tall/wide or sparse far-right sheet now errors instead of growing
gigabytes of <td></td>.
- print: decode each embedded picture once per document instead of once per
band that shows it (finding 30).
Engine: 459 tests pass; clippy clean.
Security-sweep findings from the scan at revision ac8da7d4 (storytold-
gridcraft-agent-findings.json), no longer exploitable on main at 72c1d57d.
Each change caps work or storage per sheet/part so a hostile file cannot buy
unbounded work with ordinary bytes; each has a regression test:
- finding 2 (package.rs): a part rejected by the MAX_PART size gate is
charged its declared compressed size up front, so a loop of oversized
parts still ends the allowance instead of re-entering the package.
- finding 4 (xml.rs): elements past the kept-node cap are consumed and
dropped instead of kept — parse cost stays linear, memory stops growing
with the same input (a small compressed workbook previously asked for
gigabytes of declared-sized metadata tree).
- finding 10 (records.rs): .remaining() caches the suffix of a shared
string's Continue parts, so crossing N Continue records is O(N) instead
of O(N^2) in records scanned per character.
- finding 9 (xls shared formulas): the SHRFMLA master template is parsed
once and re-emitted per dependent cell instead of decoded and reprinted
for every referencing cell.
- findings 3, 6, 8 (sheet_read.rs): hyperlink-cell expansion, conditional-
format kept range cells and dynamic-array (deleted-spill) checks are
capped with one import warning each instead of the compact-workbook
multi-gigabyte allocation or the scanned-per-cell cost.
- finding 31 (core/parse.rs): datetime recognition no longer reparses a
prefix at every space with a time-shaped tail; an all-digit field length
is gated before the derivation machinery runs, staying linear in the
text.
🤖 Generated with Codebuff
Co-authored-by: Codebuff <noreply@codebuff.com>
The status bar always showed Average, Count and Sum for a selection of
several cells and had no menu. A right-click on it now opens a menu with
Average, Count, Numerical Count, Minimum, Maximum and Sum, as in Excel.
Average, Count and Sum are on by default; the other three are off.
The text on the status bar can no longer be selected, so a right-click
on the mode text or a statistic opens the menu as well. The zoom and
view controls handle their own clicks as before.
The choice is a preference set with the new view.statusBar command and
kept in prefs.json. A prefs.json without it keeps the defaults.
Sweep findings from the ac8da7d4 security scan (storytold-gridcraft-agent-
findings.json), remaining on main at 72c1d57d:
- finding 14 (crates/calc/src/eval.rs:988): every lambda invocation now
spends one charge against a per-cell work budget (16M — one per cell of
the largest array a reference may expand to), so a hostile, deeply
nested LAMBDA/LET formula stops at the bound a legitimately wide one
could reach instead of growing exponentially with nesting. Exhaustion
is #CALC!, like the existing depth cap. Metered at `call_lambda_values`
so every path into a body is counted (direct application, MAP/REDUCE/
SCAN/BYROW/BYCOL/MAKEARRAY, D-function criteria); the criteria
sub-evaluator hands its spend back to the parent.
- finding 16 (crates/functions/src/criteria.rs:313): `match_from` takes a
step budget (1<<26: an honest match against Excel's 32,767-char cell
ceiling costs a few hundred thousand steps), and `wildcard_find` drops
the leading star run (a no-op for a prefix match) so the first literal
anchor skips straight to its occurrences; all suffix attempts share one
budget. Star churn degrades to no-match instead of quadratic work.
Unit tests: mock-host exhaustion/under-budget for lambda applications
(eval::fuel_tests), nested REDUCE under budget in full recalc, wildcard
behaviour unchanged.
🤖 Generated with Codebuff
Co-authored-by: Codebuff <noreply@codebuff.com>
The Go To dialog lists the named ranges of the workbook and of every
sheet, a name scoped to another sheet as Sheet2!Name, without hidden
names, constants or formulas. After them it lists the references
entered in the dialog, newest first, and a double-click on an entry
goes there.
edit.goTo takes several names or references separated by commas
(Price, Type or B14:C22,F19:G30) and selects them all, follows a name
that refers to several areas, and goes to Sheet2!Name on Sheet2. With
record: true it keeps a reference that holds no name among the recent
ones, and with list: true it returns the names and the recent
references.
formulas.pasteName lists the names the active sheet can use (workbook
names and the sheet's own, without hidden names, in alphabetical order).
With name it puts the name at the caret of the formula being edited,
replacing selected text, or starts =Name; with list: true it writes each
name and the text of what it refers to in two columns from the active
cell. The Paste Name dialog opens from F3 when the workbook has names,
from Use in Formula's new Paste Names… item and from Alt+M, S, P. While
a dialog is open over the edit, the editor takes no keys.
Use in Formula lists the same names, and formulas.useInFormula inserts
at the caret instead of replacing the text being edited.
- PMT: take rate / m first so (pv + fv) / m can't overflow for a tiny rate (PMT(1e-300,10,1e12)).
- Keytips probe: an empty params object keys like no params.
- Number Stored as Text check reads the same accounting parentheses as typed entry.
- Lookups: a NaN number falls back to the general comparison instead of comparing equal.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>