Skip to content

Add copy-to-split functionality for mesocycle exercise configuration#72

Merged
wulfland merged 4 commits intomainfrom
copilot/add-copy-to-split-feature
Feb 5, 2026
Merged

Add copy-to-split functionality for mesocycle exercise configuration#72
wulfland merged 4 commits intomainfrom
copilot/add-copy-to-split-feature

Conversation

Copy link
Contributor

Copilot AI commented Feb 5, 2026

Copy to Split Feature - Improvements Applied

Changes Made ✅

1. Keyboard Accessibility Enhancement (Comment #2768169844)

CopySplitDialog.tsx:

  • ✅ Added ESC key handler to close dialog
  • ✅ Implemented focus trapping to keep focus within dialog when tabbing
  • ✅ Auto-focus on Cancel button when dialog opens
  • ✅ Follows established pattern from ConfirmDialog.tsx

2. CSS Duplication Reduction (Comments #2768169779 & #2768169817)

Created shared-dialog.css:

  • ✅ Extracted common .dialog-overlay styles
  • ✅ Extracted common .btn, .btn-primary, .btn-secondary, .btn-danger styles
  • ✅ Extracted common .dialog-header, .dialog-body, .dialog-footer styles
  • ✅ Extracted common .dialog-close styles

Updated Components:

  • ✅ CopySplitDialog now imports shared-dialog.css
  • ✅ SplitDayEditor now imports shared-dialog.css
  • ✅ Removed duplicate styles from both component CSS files

Testing ✅

  • ✅ Build successful
  • ✅ Manual testing verified all functionality works
  • ✅ ESC key closes dialog correctly
  • ✅ Focus management working (Cancel button auto-focused)
  • ✅ Shared styles applied correctly across components

Screenshot

Dialog with keyboard accessibility features working:

Dialog with keyboard accessibility

Original prompt

This section details on the original issue you should resolve

<issue_title>Add "Copy to Split" feature for mesocycle exercise configuration</issue_title>
<issue_description>## User Story

As a lifter,
I want to copy exercises from one split day to another when configuring a mesocycle,
So that I can quickly set up similar training days without manually adding each exercise again.

Background

When creating mesocycles with repeating split patterns (upper/lower, full body), lifters often want similar or identical exercise selections across matching days:

  • Upper/Lower: Upper 1 and Upper 2 may share most exercises
  • Full Body: All days typically include the same compound movements
  • Push/Pull/Legs: Push 1 and Push 2, Pull 1 and Pull 2, etc.

Currently, users must manually add each exercise to every split day, which is time-consuming and error-prone.

Acceptance Criteria

  • Add a "Copy to..." button/action in the split day panel header
  • Show a dropdown or modal to select target split day(s)
  • Allow copying all exercises from the current split to the selected target(s)
  • If target already has exercises, prompt user to choose:
    • Replace existing exercises
    • Append to existing exercises
    • Cancel
  • Preserve exercise order when copying
  • Copy exercise configuration (sets, rep ranges if configured)
  • Provide visual feedback on successful copy (toast notification)
  • Works offline (local-only operation)

UI/UX Considerations

  • Copy action should be easily discoverable but not clutter the interface
  • Consider placing near the split tab or in a "..." menu for the panel
  • Clear visual indication of source and target splits
  • Undo capability would be nice-to-have

Technical Notes

  • This is a core (free) feature - local-only operation
  • No database changes required - operates on in-memory state during mesocycle configuration
  • Should integrate with existing MesocycleExerciseConfig component
  • Consider reusability for future "template" features

Out of Scope (for this issue)

  • Copying individual exercises (drag & drop)
  • Cross-mesocycle copying
  • Saving split configurations as templates

Related

  • Part of mesocycle creation/editing workflow
  • Enhances exercise configuration UX</issue_description>

Comments on the Issue (you are @copilot in this section)


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI and others added 2 commits February 5, 2026 09:39
Co-authored-by: wulfland <5276337+wulfland@users.noreply.github.com>
Co-authored-by: wulfland <5276337+wulfland@users.noreply.github.com>
Copilot AI changed the title [WIP] Add 'Copy to Split' feature for mesocycle exercise configuration Add copy-to-split functionality for mesocycle exercise configuration Feb 5, 2026
Copilot AI requested a review from wulfland February 5, 2026 09:47
@wulfland wulfland marked this pull request as ready for review February 5, 2026 10:12
@wulfland wulfland requested a review from Copilot February 5, 2026 10:13
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request adds a "Copy to Split" feature that enables users to copy exercise configurations between split days during mesocycle setup. This addresses the time-consuming process of manually duplicating similar training days (e.g., Upper 1→Upper 2, Push 1→Push 2).

Changes:

  • Added new CopySplitDialog component with multi-select target splits and replace/append mode options
  • Enhanced SplitDayEditor with a "Copy to..." button that appears when the split has exercises and multiple splits exist
  • Extended MesocycleExerciseConfig with copy state management, handlers, and toast notifications for user feedback

Reviewed changes

Copilot reviewed 5 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/components/mesocycles/CopySplitDialog.tsx New modal dialog component for selecting target splits and copy mode (replace/append)
src/components/mesocycles/CopySplitDialog.css Styling for the copy dialog including target selection, mode options, and responsive design
src/components/mesocycles/MesocycleExerciseConfig.tsx Added copy functionality with state management, handlers for initiating and confirming copy operations, and toast notifications
src/components/mesocycles/SplitDayEditor.tsx Added "Copy to..." button in header with conditional visibility based on exercise count and split availability
src/components/mesocycles/SplitDayEditor.css Added header-actions container styling and button styles for primary and secondary buttons
package-lock.json Metadata changes adding "peer": true flags to multiple packages

Comment on lines +167 to +200
.btn-secondary {
background-color: white;
color: #374151;
border: 1px solid #d1d5db;
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
font-weight: 500;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
}

.btn-secondary:hover {
background-color: #f9fafb;
border-color: #9ca3af;
}

.btn-primary {
background-color: #3b82f6;
color: white;
border: 1px solid #3b82f6;
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
font-weight: 500;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
}

.btn-primary:hover {
background-color: #2563eb;
border-color: #2563eb;
}

Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The button styles .btn-primary and .btn-secondary are being duplicated in this CSS file, but similar styles already exist in multiple other files throughout the codebase (WorkoutSession.css, CopySplitDialog.css, ExerciseForm.css, Onboarding.css, Settings.css). Consider extracting these common button styles into a global stylesheet (e.g., src/index.css or a new src/components/common/buttons.css) and importing it where needed to reduce CSS duplication and ensure consistent styling across the application.

Suggested change
.btn-secondary {
background-color: white;
color: #374151;
border: 1px solid #d1d5db;
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
font-weight: 500;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-secondary:hover {
background-color: #f9fafb;
border-color: #9ca3af;
}
.btn-primary {
background-color: #3b82f6;
color: white;
border: 1px solid #3b82f6;
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
font-weight: 500;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-primary:hover {
background-color: #2563eb;
border-color: #2563eb;
}

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit 58141d4. Created src/components/common/shared-dialog.css to extract common button styles (.btn, .btn-primary, .btn-secondary, .btn-danger, .btn-sm) that were duplicated across multiple files. Both CopySplitDialog and SplitDayEditor now import this shared stylesheet.

Dialog with shared styles

Comment on lines +1 to +237
.copy-split-dialog {
background: white;
border-radius: 8px;
box-shadow:
0 20px 25px -5px rgba(0, 0, 0, 0.1),
0 10px 10px -5px rgba(0, 0, 0, 0.04);
max-width: 500px;
width: 100%;
max-height: 90vh;
overflow-y: auto;
}

.copy-split-dialog .dialog-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem;
border-bottom: 1px solid #e5e7eb;
}

.copy-split-dialog .dialog-header h2 {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
color: #1f2937;
}

.copy-split-dialog .dialog-close {
background: transparent;
border: none;
font-size: 2rem;
line-height: 1;
color: #6b7280;
cursor: pointer;
padding: 0;
width: 2rem;
height: 2rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: all 0.2s ease;
}

.copy-split-dialog .dialog-close:hover {
background-color: #f3f4f6;
color: #1f2937;
}

.copy-split-dialog .dialog-body {
padding: 1.5rem;
}

.dialog-description {
margin: 0 0 1rem 0;
color: #4b5563;
line-height: 1.5;
font-size: 0.875rem;
}

.target-selection {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-bottom: 1rem;
}

.target-option {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
border: 2px solid #e5e7eb;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
}

.target-option:hover {
background-color: #f9fafb;
border-color: #d1d5db;
}

.target-option.selected {
border-color: #3b82f6;
background-color: #eff6ff;
}

.target-option input[type='checkbox'] {
cursor: pointer;
width: 1rem;
height: 1rem;
accent-color: #3b82f6;
}

.target-name {
flex: 1;
font-weight: 500;
color: #1f2937;
}

.target-badge {
font-size: 0.75rem;
color: #6b7280;
background-color: #f3f4f6;
padding: 0.25rem 0.5rem;
border-radius: 9999px;
}

.empty-targets {
text-align: center;
padding: 2rem;
color: #6b7280;
font-size: 0.875rem;
margin: 0;
}

.copy-mode-section {
margin-top: 1.5rem;
padding-top: 1.5rem;
border-top: 1px solid #e5e7eb;
}

.copy-mode-label {
margin: 0 0 0.75rem 0;
font-size: 0.875rem;
font-weight: 500;
color: #374151;
}

.copy-mode-options {
display: flex;
flex-direction: column;
gap: 0.5rem;
}

.mode-option {
display: flex;
gap: 0.75rem;
padding: 0.75rem;
border: 2px solid #e5e7eb;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
}

.mode-option:hover {
background-color: #f9fafb;
border-color: #d1d5db;
}

.mode-option.selected {
border-color: #3b82f6;
background-color: #eff6ff;
}

.mode-option input[type='radio'] {
cursor: pointer;
width: 1rem;
height: 1rem;
margin-top: 0.125rem;
accent-color: #3b82f6;
}

.mode-content {
display: flex;
flex-direction: column;
gap: 0.25rem;
flex: 1;
}

.mode-content strong {
font-weight: 600;
color: #1f2937;
}

.mode-hint {
font-size: 0.75rem;
color: #6b7280;
line-height: 1.4;
}

.copy-split-dialog .dialog-footer {
display: flex;
justify-content: flex-end;
gap: 0.75rem;
padding: 1.5rem;
border-top: 1px solid #e5e7eb;
}

.btn {
padding: 0.625rem 1.25rem;
font-size: 0.875rem;
font-weight: 500;
border-radius: 6px;
border: 1px solid transparent;
cursor: pointer;
transition: all 0.2s ease;
}

.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}

.btn-secondary {
background-color: white;
color: #374151;
border-color: #d1d5db;
}

.btn-secondary:hover:not(:disabled) {
background-color: #f9fafb;
border-color: #9ca3af;
}

.btn-primary {
background-color: #3b82f6;
color: white;
}

.btn-primary:hover:not(:disabled) {
background-color: #2563eb;
}

@media (max-width: 640px) {
.copy-split-dialog {
max-width: 100%;
margin: 0 1rem;
}

.copy-split-dialog .dialog-header,
.copy-split-dialog .dialog-body,
.copy-split-dialog .dialog-footer {
padding: 1rem;
}
}
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The .dialog-overlay class is being defined in this new CSS file, but the same class with identical styling already exists in multiple files (ConfirmDialog.css, DeleteConfirmDialog.css, MesocycleForm.css, WorkoutSession.css). Consider extracting this common dialog overlay style into a global stylesheet to avoid duplication and ensure consistency. The same applies to the .btn, .btn-primary, and .btn-secondary classes defined later in this file.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit 58141d4. Created src/components/common/shared-dialog.css to extract common dialog styles (.dialog-overlay, .dialog-header, .dialog-body, .dialog-footer, .dialog-close) and button styles that were duplicated across multiple files. This ensures consistency and reduces duplication throughout the codebase.

Comment on lines +16 to +173
export default function CopySplitDialog({
sourceSplitDay,
availableSplitDays,
onConfirm,
onCancel,
}: CopySplitDialogProps) {
const [selectedTargetIds, setSelectedTargetIds] = useState<string[]>([]);
const [copyMode, setCopyMode] = useState<'replace' | 'append'>('replace');

const handleToggleTarget = (targetId: string) => {
setSelectedTargetIds((prev) =>
prev.includes(targetId)
? prev.filter((id) => id !== targetId)
: [...prev, targetId]
);
};

const handleConfirm = () => {
if (selectedTargetIds.length === 0) return;
onConfirm(selectedTargetIds, copyMode);
};

// Filter out the source split day from available targets
const targetSplitDays = availableSplitDays.filter(
(day) => day.id !== sourceSplitDay.id
);

// Check which targets have exercises
const targetsWithExercises = targetSplitDays.filter(
(day) => day.exercises.length > 0
);

const hasSelectedTargetsWithExercises = selectedTargetIds.some((id) =>
targetsWithExercises.find((day) => day.id === id)
);

return (
<div
className="dialog-overlay"
onClick={onCancel}
role="dialog"
aria-modal="true"
aria-labelledby="copy-dialog-title"
>
<div className="copy-split-dialog" onClick={(e) => e.stopPropagation()}>
<div className="dialog-header">
<h2 id="copy-dialog-title">Copy Exercises</h2>
<button
className="dialog-close"
onClick={onCancel}
aria-label="Close dialog"
>
×
</button>
</div>

<div className="dialog-body">
<p className="dialog-description">
Copy {sourceSplitDay.exercises.length} exercise
{sourceSplitDay.exercises.length !== 1 ? 's' : ''} from{' '}
<strong>{sourceSplitDay.name}</strong> to:
</p>

<div className="target-selection">
{targetSplitDays.map((splitDay) => {
const isSelected = selectedTargetIds.includes(splitDay.id);
const hasExercises = splitDay.exercises.length > 0;

return (
<label
key={splitDay.id}
className={`target-option ${isSelected ? 'selected' : ''}`}
>
<input
type="checkbox"
checked={isSelected}
onChange={() => handleToggleTarget(splitDay.id)}
/>
<span className="target-name">{splitDay.name}</span>
{hasExercises && (
<span className="target-badge">
{splitDay.exercises.length} exercise
{splitDay.exercises.length !== 1 ? 's' : ''}
</span>
)}
</label>
);
})}
</div>

{targetSplitDays.length === 0 && (
<p className="empty-targets">
No other split days available to copy to.
</p>
)}

{hasSelectedTargetsWithExercises && (
<div className="copy-mode-section">
<p className="copy-mode-label">
One or more selected targets already have exercises:
</p>
<div className="copy-mode-options">
<label
className={`mode-option ${copyMode === 'replace' ? 'selected' : ''}`}
>
<input
type="radio"
name="copyMode"
value="replace"
checked={copyMode === 'replace'}
onChange={() => setCopyMode('replace')}
/>
<div className="mode-content">
<strong>Replace</strong>
<span className="mode-hint">
Remove existing exercises and replace with copied ones
</span>
</div>
</label>
<label
className={`mode-option ${copyMode === 'append' ? 'selected' : ''}`}
>
<input
type="radio"
name="copyMode"
value="append"
checked={copyMode === 'append'}
onChange={() => setCopyMode('append')}
/>
<div className="mode-content">
<strong>Append</strong>
<span className="mode-hint">
Add copied exercises after existing ones
</span>
</div>
</label>
</div>
</div>
)}
</div>

<div className="dialog-footer">
<button className="btn btn-secondary" onClick={onCancel}>
Cancel
</button>
<button
className="btn btn-primary"
onClick={handleConfirm}
disabled={selectedTargetIds.length === 0}
>
Copy to {selectedTargetIds.length || 0} split
{selectedTargetIds.length !== 1 ? 's' : ''}
</button>
</div>
</div>
</div>
);
}
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This dialog is missing keyboard accessibility features that are implemented in other dialogs in the codebase (e.g., ConfirmDialog.tsx). Consider adding:

  1. Escape key handler to close the dialog
  2. Focus trapping to keep focus within the dialog when tabbing
  3. Auto-focus on the first interactive element when the dialog opens

These features improve accessibility and match the patterns established elsewhere in the codebase. You can reference ConfirmDialog.tsx (lines 30-67) for an implementation example.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit 58141d4. Added keyboard accessibility features to CopySplitDialog:

  • ESC key handler to close dialog
  • Focus trapping to keep focus within dialog when tabbing
  • Auto-focus on Cancel button when dialog opens

Implementation follows the pattern from ConfirmDialog.tsx as suggested.

Dialog with keyboard accessibility

@wulfland
Copy link
Owner

wulfland commented Feb 5, 2026

@copilot apply changes based on the comments in this thread

Co-authored-by: wulfland <5276337+wulfland@users.noreply.github.com>
Copilot AI requested a review from wulfland February 5, 2026 10:31
@wulfland wulfland enabled auto-merge February 5, 2026 10:38
@wulfland wulfland merged commit c70eb48 into main Feb 5, 2026
4 checks passed
@wulfland wulfland deleted the copilot/add-copy-to-split-feature branch February 5, 2026 10:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add "Copy to Split" feature for mesocycle exercise configuration

3 participants