mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-07 14:08:38 +00:00
feat: interface zoom control settings (#7255)
* feat: interface zoom control settings * fix: allow zoom controls using shortcuts * fix: maintain consitency in zoom shortcuts and ui interface * fix: added min max to 50% and 150% for zoom * fix: moved percentageToZoomLevel function in bruno-common * chore: abstractions --------- Co-authored-by: shubh-bruno <shubh-bruno@shubh-bruno.local>
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
color: ${(props) => props.theme.text};
|
||||
|
||||
.zoom-field {
|
||||
min-width: 120px;
|
||||
max-width: 150px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.zoom-field label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.5rem;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.custom-select {
|
||||
width: 100%;
|
||||
height: 2.25rem;
|
||||
padding: 0 2rem 0 0.625rem;
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
background-color: ${(props) => props.theme.input.background};
|
||||
border: 1px solid ${(props) => props.theme.input.border};
|
||||
border-radius: 0.375rem;
|
||||
color: ${(props) => props.theme.text};
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25;
|
||||
transition: all 0.15s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.custom-select:hover {
|
||||
border-color: ${(props) => props.theme.input.hoverBorder || props.theme.input.border};
|
||||
}
|
||||
|
||||
.custom-select .selected-value {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.custom-select .chevron-icon {
|
||||
color: ${(props) => props.theme.input.border};
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin-top: 0.25rem;
|
||||
background-color: ${(props) => props.theme.input.background};
|
||||
border: 1px solid ${(props) => props.theme.input.border};
|
||||
border-radius: 0.375rem;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
z-index: 50;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.dropdown-option {
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
transition: background-color 0.1s ease;
|
||||
}
|
||||
|
||||
.dropdown-option:hover {
|
||||
background-color: ${(props) => props.theme.input.border};
|
||||
}
|
||||
|
||||
.dropdown-option.selected {
|
||||
background-color: ${(props) => props.theme.input.focusBorder || props.theme.input.border}22;
|
||||
}
|
||||
|
||||
.dropdown-option .option-label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.dropdown-option .check-icon {
|
||||
color: ${(props) => props.theme.textLink};
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.reset-btn {
|
||||
padding: 0.5rem 1rem;
|
||||
height: 2.25rem;
|
||||
background: transparent;
|
||||
border: 1px solid ${(props) => props.theme.input.border};
|
||||
border-radius: 0.375rem;
|
||||
color: ${(props) => props.theme.textLink};
|
||||
font-size: 0.875rem;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.input.border};
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: ${(props) => props.theme.input.focusBorder};
|
||||
box-shadow: 0 0 0 2px ${(props) => props.theme.input.focusBorder}33;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,112 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { savePreferences } from 'providers/ReduxStore/slices/app';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { IconChevronDown, IconCheck } from '@tabler/icons';
|
||||
const { percentageToZoomLevel } = require('@usebruno/common');
|
||||
|
||||
// Zoom options for dropdown (50% to 150%)
|
||||
const ZOOM_OPTIONS = [
|
||||
{ label: '50%', value: 50 },
|
||||
{ label: '60%', value: 60 },
|
||||
{ label: '70%', value: 70 },
|
||||
{ label: '80%', value: 80 },
|
||||
{ label: '90%', value: 90 },
|
||||
{ label: '100%', value: 100 },
|
||||
{ label: '110%', value: 110 },
|
||||
{ label: '120%', value: 120 },
|
||||
{ label: '130%', value: 130 },
|
||||
{ label: '140%', value: 140 },
|
||||
{ label: '150%', value: 150 }
|
||||
];
|
||||
|
||||
const DEFAULT_ZOOM = 100;
|
||||
|
||||
const Zoom = () => {
|
||||
const dispatch = useDispatch();
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
const dropdownRef = useRef(null);
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
// Get saved zoom percentage from Redux preferences (single source of truth)
|
||||
const savedZoom = get(preferences, 'display.zoomPercentage', DEFAULT_ZOOM);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
const handleSelect = (zoom) => {
|
||||
// Apply zoom level to Electron window immediately
|
||||
if (ipcRenderer) {
|
||||
const zoomLevel = percentageToZoomLevel(zoom);
|
||||
ipcRenderer.invoke('renderer:set-zoom-level', zoomLevel);
|
||||
}
|
||||
|
||||
// Save to preferences via Redux (same pattern as layout)
|
||||
const updatedPreferences = {
|
||||
...preferences,
|
||||
display: {
|
||||
...get(preferences, 'display', {}),
|
||||
zoomPercentage: zoom
|
||||
}
|
||||
};
|
||||
dispatch(savePreferences(updatedPreferences));
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const handleResetToDefault = () => {
|
||||
handleSelect(DEFAULT_ZOOM);
|
||||
};
|
||||
|
||||
const selectedOption = ZOOM_OPTIONS.find((opt) => opt.value === savedZoom);
|
||||
const isDefault = savedZoom === DEFAULT_ZOOM;
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="flex flex-row gap-4 items-end">
|
||||
<div className="zoom-field" ref={dropdownRef}>
|
||||
<label className="block">Interface Zoom</label>
|
||||
<div className="custom-select mt-2" onClick={() => setIsOpen(!isOpen)}>
|
||||
<span className="selected-value">{selectedOption?.label}</span>
|
||||
<IconChevronDown size={14} className="chevron-icon" />
|
||||
</div>
|
||||
{isOpen && (
|
||||
<div className="dropdown-menu">
|
||||
{ZOOM_OPTIONS.map((option) => (
|
||||
<div
|
||||
key={option.value}
|
||||
className={`dropdown-option ${option.value === savedZoom ? 'selected' : ''}`}
|
||||
onClick={() => handleSelect(option.value)}
|
||||
>
|
||||
<span className="option-label">{option.label}</span>
|
||||
{option.value === savedZoom && <IconCheck size={14} className="check-icon" />}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!isDefault && (
|
||||
<button
|
||||
type="button"
|
||||
className="reset-btn"
|
||||
onClick={handleResetToDefault}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Zoom;
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import Font from './Font/index';
|
||||
import Zoom from './Zoom/index';
|
||||
|
||||
const Display = ({ close }) => {
|
||||
return (
|
||||
@@ -9,6 +10,9 @@ const Display = ({ close }) => {
|
||||
<div className="w-fit flex flex-col gap-2">
|
||||
<Font close={close} />
|
||||
</div>
|
||||
<div className="w-full flex flex-col gap-2">
|
||||
<Zoom />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user