Feature/v2/right rail (#4255)

# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Co-authored-by: ConnorYoh <[email protected]>
Co-authored-by: Anthony Stirling <[email protected]>
This commit is contained in:
EthanHealy01
2025-08-25 12:53:33 +01:00
committed by GitHub
co-authored by ConnorYoh Anthony Stirling
parent 61f5221c58
commit 895bcebc7b
24 changed files with 1070 additions and 329 deletions
@@ -0,0 +1,108 @@
# RightRail Component
A dynamic vertical toolbar on the right side of the application that supports both static buttons (Undo/Redo, Save, Print, Share) and dynamic buttons registered by tools.
## Structure
- **Top Section**: Dynamic buttons from tools (empty when none)
- **Middle Section**: Grid, Cut, Undo, Redo
- **Bottom Section**: Save, Print, Share
## Usage
### For Tools (Recommended)
```tsx
import { useRightRailButtons } from '../hooks/useRightRailButtons';
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
function MyTool() {
const handleAction = useCallback(() => {
// Your action here
}, []);
useRightRailButtons([
{
id: 'my-action',
icon: <PlayArrowIcon />,
tooltip: 'Execute Action',
onClick: handleAction,
},
]);
return <div>My Tool</div>;
}
```
### Multiple Buttons
```tsx
useRightRailButtons([
{
id: 'primary',
icon: <StarIcon />,
tooltip: 'Primary Action',
order: 1,
onClick: handlePrimary,
},
{
id: 'secondary',
icon: <SettingsIcon />,
tooltip: 'Secondary Action',
order: 2,
onClick: handleSecondary,
},
]);
```
### Conditional Buttons
```tsx
useRightRailButtons([
// Always show
{
id: 'process',
icon: <PlayArrowIcon />,
tooltip: 'Process',
disabled: isProcessing,
onClick: handleProcess,
},
// Only show when condition met
...(hasResults ? [{
id: 'export',
icon: <DownloadIcon />,
tooltip: 'Export',
onClick: handleExport,
}] : []),
]);
```
## API
### Button Config
```typescript
interface RightRailButtonWithAction {
id: string; // Unique identifier
icon: React.ReactNode; // Icon component
tooltip: string; // Hover tooltip
section?: 'top' | 'middle' | 'bottom'; // Section (default: 'top')
order?: number; // Sort order (default: 0)
disabled?: boolean; // Disabled state (default: false)
visible?: boolean; // Visibility (default: true)
onClick: () => void; // Click handler
}
```
## Built-in Features
- **Undo/Redo**: Automatically integrates with Page Editor
- **Theme Support**: Light/dark mode with CSS variables
- **Auto Cleanup**: Buttons unregister when tool unmounts
## Best Practices
- Use descriptive IDs: `'compress-optimize'`, `'ocr-process'`
- Choose appropriate Material-UI icons
- Keep tooltips concise: `'Compress PDF'`, `'Process with OCR'`
- Use `useCallback` for click handlers to prevent re-registration
@@ -0,0 +1,127 @@
.right-rail {
background-color: var(--right-rail-bg);
width: 3.5rem;
min-width: 3.5rem;
max-width: 3.5rem;
position: relative;
z-index: 10;
display: flex;
flex-direction: column;
height: 100vh;
border-left: 1px solid var(--border-subtle);
}
.right-rail-inner {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.75rem;
padding: 1rem 0.5rem;
}
.right-rail-section {
background-color: var(--right-rail-foreground);
border-radius: 12px;
padding: 0.5rem;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.75rem;
}
.right-rail-divider {
width: 2.75rem;
border: none;
border-top: 1px solid var(--tool-subcategory-rule-color);
margin: 0.25rem 0;
}
.right-rail-icon {
color: var(--right-rail-icon);
}
.right-rail-icon[aria-disabled="true"],
.right-rail-icon[disabled] {
color: var(--right-rail-icon-disabled) !important;
background-color: transparent !important;
}
.right-rail-spacer {
flex: 1;
}
/* Animated grow-down slot for buttons (mirrors current-tool-slot behavior) */
.right-rail-slot {
overflow: hidden;
max-height: 0;
opacity: 0;
transition: max-height 450ms ease-out, opacity 300ms ease-out;
}
.right-rail-enter {
animation: rightRailGrowDown 450ms ease-out;
}
.right-rail-exit {
animation: rightRailShrinkUp 450ms ease-out;
}
.right-rail-slot.visible {
max-height: 18rem; /* increased to fit additional controls + divider */
opacity: 1;
}
@keyframes rightRailGrowDown {
0% {
max-height: 0;
opacity: 0;
}
100% {
max-height: 18rem;
opacity: 1;
}
}
@keyframes rightRailShrinkUp {
0% {
max-height: 18rem;
opacity: 1;
}
100% {
max-height: 0;
opacity: 0;
}
}
/* Remove bottom margin from close icon */
.right-rail-slot .right-rail-icon {
margin-bottom: 0;
}
/* Inline appear/disappear animation for page-number selector button */
.right-rail-fade {
transition-property: opacity, transform, max-height, visibility;
transition-duration: 220ms, 220ms, 220ms, 0s;
transition-timing-function: ease, ease, ease, linear;
transition-delay: 0s, 0s, 0s, 0s;
transform-origin: top center;
overflow: hidden;
}
.right-rail-fade.enter {
opacity: 1;
transform: scale(1);
max-height: 3rem;
visibility: visible;
}
.right-rail-fade.exit {
opacity: 0;
transform: scale(0.85);
max-height: 0;
visibility: hidden;
/* delay visibility change so opacity/max-height can finish */
transition-delay: 0s, 0s, 0s, 220ms;
pointer-events: none;
}