feat(desktop): gate traffic-lights behind window-controls style setting

Add a Window controls Style setting ('classic' | 'traffic-lights',
default 'classic') next to the existing Position setting. All four
position×style combinations render correctly with side-driven order
(close,min,max left; min,max,close right).

- Restore deleted left-classic branch (h-8 w-8 rounded-md buttons)
- Add right-traffic-lights branch (cluster with ml-1 container)
- Branch on style instead of side in WindowsWindowControls
- Store field + setter with 'classic' default (no migration needed;
  Zustand persist shallow-merges over initial state)
- Settings UI: SettingsTwoColumn with Position chips + Style dropdown
- Sanitize on client (persistence.ts) and server (settings-helpers.js)
- 10 locale dictionaries updated (add-only, no existing keys changed)
- Traffic-light button spacing 8px→10px edge-to-edge
- Delete dead DESKTOP_WINDOW_CONTROLS_WIDTH_PX constant
This commit is contained in:
Pablo Gonzalez
2026-07-29 19:38:48 +02:00
parent 029f1705db
commit 9001e38c54
19 changed files with 80 additions and 162 deletions
@@ -16,24 +16,24 @@ const TRAFFIC_LIGHT_COLORS: Record<DesktopWindowControlAction, { fill: string; g
maximize: { fill: '#28C940', glyph: '#006200' },
};
const TrafficLightGlyph: React.FC<{ action: DesktopWindowControlAction }> = ({ action }) => {
const TrafficLightGlyph: React.FC<{ action: DesktopWindowControlAction; isMaximized: boolean }> = ({ action, isMaximized }) => {
if (action === 'close') {
return (
<svg viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth={1.2} strokeLinecap="round" className="size-[9px]" aria-hidden>
<path d="M3.5 3.5 L8.5 8.5 M8.5 3.5 L3.5 8.5" />
</svg>
);
return <Icon name="close" className="size-[10px]" />;
}
if (action === 'minimize') {
return (
<svg viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth={1.4} strokeLinecap="round" className="size-[9px]" aria-hidden>
<path d="M3 6 L9 6" />
</svg>
);
return <Icon name="subtract" className="size-[10px]" />;
}
// Green expand: two paired corner triangles pointing diagonally outward.
return (
<svg viewBox="0 0 12 12" fill="currentColor" className="size-[9px]" aria-hidden>
// Green expand: two filled corner triangles pointing outward (maximize)
// or inward (restore). No Remixicon equivalent exists.
// Glyphs are 10px inside the 14px circle on purpose: 9px left a half-pixel
// (2.5px) padding that rasterized lopsided at 1x DPR.
return isMaximized ? (
<svg viewBox="0 0 12 12" fill="currentColor" className="size-[10px]" aria-hidden>
<path d="M5.25 6.75 L9.75 6.75 L9.75 2.25 Z" />
<path d="M6.75 5.25 L2.25 5.25 L2.25 9.75 Z" />
</svg>
) : (
<svg viewBox="0 0 12 12" fill="currentColor" className="size-[10px]" aria-hidden>
<path d="M5.25 2.25 L9.75 2.25 L9.75 6.75 Z" />
<path d="M6.75 9.75 L2.25 9.75 L2.25 5.25 Z" />
</svg>
@@ -72,8 +72,8 @@ const TrafficLightButton: React.FC<TrafficLightButtonProps> = ({ action, isMaxim
className="flex size-3.5 items-center justify-center rounded-full shadow-[inset_0_0_0_0.5px_rgba(0,0,0,0.28)] transition-[filter] duration-75 active:brightness-90"
style={{ backgroundColor: fill, color: glyph }}
>
<span className="flex opacity-0 transition-opacity duration-75 group-hover/wctl:opacity-100">
<TrafficLightGlyph action={action} />
<span className="flex items-center justify-center opacity-0 transition-opacity duration-75 group-hover/wctl:opacity-100">
<TrafficLightGlyph action={action} isMaximized={isMaximized} />
</span>
</span>
</button>