fix(properties): harden recovery and window chrome

This commit is contained in:
NimBold
2026-08-05 21:41:59 +03:30
parent c1202229ac
commit b36f55e5b5
16 changed files with 527 additions and 132 deletions
+21
View File
@@ -0,0 +1,21 @@
import { describe, expect, it } from 'vitest';
import { getPropertiesFooterActions } from './propertiesFooter';
describe('Properties footer state', () => {
it('shows discard, save, and close for ordinary dirty edits', () => {
expect(getPropertiesFooterActions({ isDirty: true, hasUnsavedNavigation: false }))
.toEqual(['discardChanges', 'save', 'close']);
});
it('shows discard, save, and keep editing for an unsaved tab or close prompt', () => {
expect(getPropertiesFooterActions({ isDirty: true, hasUnsavedNavigation: true }))
.toEqual(['discardChanges', 'save', 'keepEditing']);
expect(getPropertiesFooterActions({ isDirty: false, hasUnsavedNavigation: true }))
.toEqual(['discardChanges', 'save', 'keepEditing']);
});
it('keeps close available when there are no edits', () => {
expect(getPropertiesFooterActions({ isDirty: false, hasUnsavedNavigation: false }))
.toEqual(['close']);
});
});
+15
View File
@@ -0,0 +1,15 @@
export type PropertiesFooterAction = 'discardChanges' | 'save' | 'close' | 'keepEditing';
export type PropertiesFooterState = {
isDirty: boolean;
hasUnsavedNavigation: boolean;
};
export const getPropertiesFooterActions = ({
isDirty,
hasUnsavedNavigation,
}: PropertiesFooterState): PropertiesFooterAction[] => {
if (hasUnsavedNavigation) return ['discardChanges', 'save', 'keepEditing'];
if (isDirty) return ['discardChanges', 'save', 'close'];
return ['close'];
};
+12 -1
View File
@@ -1,5 +1,9 @@
import { describe, expect, it } from 'vitest';
import { getWindowControlRevealOffset, resolveWindowControlStyle } from './windowControlStyle';
import {
getWindowControlRevealOffset,
resolveWindowControlSide,
resolveWindowControlStyle,
} from './windowControlStyle';
describe('resolveWindowControlStyle', () => {
it('uses the platform convention for automatic style', () => {
@@ -32,4 +36,11 @@ describe('resolveWindowControlStyle', () => {
expect(getWindowControlRevealOffset('gnome')).toBe(134);
expect(getWindowControlRevealOffset('minimal')).toBe(104);
});
it('resolves automatic control placement from the effective document direction', () => {
expect(resolveWindowControlSide('auto', 'ltr')).toBe('left');
expect(resolveWindowControlSide('auto', 'rtl')).toBe('right');
expect(resolveWindowControlSide('left', 'rtl')).toBe('left');
expect(resolveWindowControlSide('right', 'ltr')).toBe('right');
});
});
+10
View File
@@ -1,6 +1,8 @@
import type { WindowControlStyle } from '../bindings/WindowControlStyle';
export type ResolvedWindowControlStyle = Exclude<WindowControlStyle, 'auto'>;
export type WindowControlSide = 'left' | 'right';
export type SidebarPosition = 'auto' | WindowControlSide;
// The reveal button sits after the complete custom-control hit area. Keep this
// derived from the resolved style so a sidebar toggle can never overlap a
@@ -15,6 +17,14 @@ const WINDOW_CONTROL_REVEAL_OFFSETS: Record<ResolvedWindowControlStyle, number>
export const getWindowControlRevealOffset = (style: ResolvedWindowControlStyle): number =>
WINDOW_CONTROL_REVEAL_OFFSETS[style];
export const resolveWindowControlSide = (
sidebarPosition: SidebarPosition,
direction: 'ltr' | 'rtl',
): WindowControlSide => sidebarPosition === 'right'
|| (sidebarPosition === 'auto' && direction === 'rtl')
? 'right'
: 'left';
export const resolveWindowControlStyle = (
style: WindowControlStyle,
os: string,