···55The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7788+## [0.23.1] — 2026-04-06
99+1010+### Fixed
1111+- Landing page event listener accumulation: convert all render-cycle listeners to event delegation (#362)
1212+- IFERROR now correctly catches error strings (#DIV/0!, #REF!, #VALUE!, #NAME?) instead of only JS exceptions (#362)
1313+- Diagram arrowhead marker creation moved from render loop to initialization (#362)
1414+815## [0.23.0] — 2026-04-04
9161017### Added
1818+- Fit and finish polish pass — fix rough edges, improve tests and QA (#359)
1119- AI companion chat panel in diagrams mode with shape/arrow actions (#355)
1220- AI companion chat panel in slides mode with slide/text/shape actions (#356)
1321- AI companion chat panel in forms mode with question add/modify/remove actions (#357)
···2129## [0.22.4] — 2026-04-04
22302331### Fixed
3232+- Fix XSS vulnerabilities and server/formula bugs found in audit (#360)
2433- Fix diagrams saving as spreadsheet when reopened (#353)
2534- Fix inline text editing not working in diagram mode (#352)
2635- Inline text editing in diagrams survives re-renders (textarea no longer destroyed) (#352)
···216225- Fix E2E test flakiness: replace page reload with addInitScript, add waitForURL before waitForSelector (#305)
217226218227### Changed
228228+- Fit and finish round 3: provider listener leak, fetch error handling, test gaps (#361)
219229- Add E2E tests for database views (kanban, gallery, calendar, timeline, pivot) (#302)
220230- Add E2E tests for forms builder and submission (#301)
221231- Add E2E tests for diagrams whiteboard (#300)
···791791 case 'AND': return flat(args).every(Boolean);
792792 case 'OR': return flat(args).some(Boolean);
793793 case 'NOT': return !args[0];
794794- case 'IFERROR': { try { return args[0]; } catch { return args[1] ?? ''; } }
794794+ case 'IFERROR': {
795795+ const val = args[0];
796796+ if (typeof val === 'string' && val.startsWith('#')) return args[1] ?? '';
797797+ return val;
798798+ }
795799796800 case 'CONCATENATE': return flat(args).map(String).join('');
797801 case 'LEN': return String(args[0]).length;
+15-5
tests/formulas-edge-cases.test.ts
···595595 expect(evalWith('IFERROR("hello","err")')).toBe('hello');
596596 });
597597598598- it('IFERROR wrapping unknown function still returns the error', () => {
599599- // FAKEFN produces #NAME? but IFERROR may not catch it since it's a returned value, not an exception
598598+ it('IFERROR wrapping unknown function catches the error', () => {
599599+ // FAKEFN produces #NAME? which starts with # — IFERROR catches it
600600 const result = evalWith('IFERROR(FAKEFN(1),"fallback")');
601601- // IFERROR only catches thrown errors; #NAME? is a returned string value
602602- // So it may return the #NAME? string directly
603603- expect(typeof result).toBe('string');
601601+ expect(result).toBe('fallback');
602602+ });
603603+604604+ it('IFERROR catches #REF! errors', () => {
605605+ expect(evalWith('IFERROR(#REF!,"fixed")')).toBe('fixed');
606606+ });
607607+608608+ it('IFERROR catches #VALUE! errors', () => {
609609+ expect(evalWith('IFERROR(#VALUE!,"fixed")')).toBe('fixed');
610610+ });
611611+612612+ it('IFERROR does not catch strings that happen to contain #', () => {
613613+ expect(evalWith('IFERROR("hello #world","err")')).toBe('hello #world');
604614 });
605615});
606616
+3-3
tests/formulas-security.test.ts
···185185 expect(result).toBe('#DIV/0!');
186186 });
187187188188- it('IFERROR with division by zero returns the error string', () => {
188188+ it('IFERROR with division by zero catches error string', () => {
189189 const result = evalWith('IFERROR(1/0,"safe")');
190190- // 1/0 = '#DIV/0!' string; IFERROR only catches thrown exceptions, not error strings
191191- expect(result).toBe('#DIV/0!');
190190+ // 1/0 = '#DIV/0!' string; IFERROR now catches error strings starting with #
191191+ expect(result).toBe('safe');
192192 });
193193});
194194