diff --git a/client/src/components/Packing/ApplyTemplateButton.tsx b/client/src/components/Packing/ApplyTemplateButton.tsx index 70bd13187..239137927 100644 --- a/client/src/components/Packing/ApplyTemplateButton.tsx +++ b/client/src/components/Packing/ApplyTemplateButton.tsx @@ -4,6 +4,7 @@ import { packingApi } from '../../api/client' import { useTripStore } from '../../store/tripStore' import { useToast } from '../shared/Toast' import { useTranslation } from '../../i18n' +import { PackingTemplateOption } from './PackingTemplateOption' interface Template { id: number @@ -79,23 +80,7 @@ export default function ApplyTemplateButton({ tripId, visibility, style, classNa }} > {templates.map(tmpl => ( - handleApply(tmpl.id)} - style={{ - display: 'flex', alignItems: 'center', gap: 8, width: '100%', - padding: '8px 12px', borderRadius: 8, border: 'none', cursor: 'pointer', - background: 'transparent', fontFamily: 'inherit', fontSize: 'calc(12px * var(--fs-scale-body, 1))', color: 'var(--text-primary)', - }} - onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-tertiary)'} - onMouseLeave={e => e.currentTarget.style.background = 'transparent'} - > - - - {tmpl.name} - - {tmpl.item_count} {t('admin.packingTemplates.items', { count: tmpl.item_count })} - - - + handleApply(tmpl.id)} t={t} /> ))} )} diff --git a/client/src/components/Packing/PackingListPanelHeader.tsx b/client/src/components/Packing/PackingListPanelHeader.tsx index aa11c5b86..d445a2eab 100644 --- a/client/src/components/Packing/PackingListPanelHeader.tsx +++ b/client/src/components/Packing/PackingListPanelHeader.tsx @@ -3,6 +3,7 @@ import { } from 'lucide-react' import type { PackingState } from './usePackingListPanel' import NameDialog from '../shared/NameDialog' +import { PackingTemplateOption } from './PackingTemplateOption' export function PackingHeader(S: PackingState) { const { @@ -63,22 +64,14 @@ export function PackingHeader(S: PackingState) { boxShadow: '0 4px 16px rgba(0,0,0,0.12)', padding: 4, minWidth: 200, }}> {availableTemplates.map(tmpl => ( - handleApplyTemplate(tmpl.id)} - style={{ - display: 'flex', alignItems: 'center', gap: 8, width: '100%', - padding: '8px 12px', borderRadius: 8, border: 'none', cursor: 'pointer', - background: 'transparent', fontFamily: 'inherit', fontSize: 'calc(12px * var(--fs-scale-body, 1))', color: 'var(--text-primary)', - transition: 'background 0.1s', - }} - onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-tertiary)'} - onMouseLeave={e => e.currentTarget.style.background = 'transparent'} - > - - - {tmpl.name} - {tmpl.item_count} {t('admin.packingTemplates.items', { count: tmpl.item_count })} - - + handleApplyTemplate(tmpl.id)} + t={t} + fade + /> ))} )} diff --git a/client/src/components/Packing/PackingTemplateOption.test.tsx b/client/src/components/Packing/PackingTemplateOption.test.tsx new file mode 100644 index 000000000..17eddb1e4 --- /dev/null +++ b/client/src/components/Packing/PackingTemplateOption.test.tsx @@ -0,0 +1,38 @@ +import { describe, expect, it, vi } from 'vitest'; +import { fireEvent, render, screen } from '../../../tests/helpers/render'; +import { PackingTemplateOption } from './PackingTemplateOption'; + +const t = (key: string, params?: Record) => + key === 'admin.packingTemplates.items' ? `items(${params?.count})` : key; + +describe('PackingTemplateOption', () => { + it('shows the template name over its item count', () => { + render(); + expect(screen.getByText('Beach')).toBeInTheDocument(); + expect(screen.getByText('12 items(12)')).toBeInTheDocument(); + }); + + it('applies the template on click', () => { + const onClick = vi.fn(); + render(); + fireEvent.click(screen.getByRole('button')); + expect(onClick).toHaveBeenCalledTimes(1); + }); + + it('tints the row while hovered and clears it on leave', () => { + render(); + const row = screen.getByRole('button'); + fireEvent.mouseEnter(row); + expect(row.style.background).toBe('var(--bg-tertiary)'); + fireEvent.mouseLeave(row); + expect(row.style.background).toBe('transparent'); + }); + + it('fades the hover only when asked', () => { + const { unmount } = render(); + expect(screen.getByRole('button').style.transition).toBe(''); + unmount(); + render(); + expect(screen.getByRole('button').style.transition).toBe('background 0.1s'); + }); +}); diff --git a/client/src/components/Packing/PackingTemplateOption.tsx b/client/src/components/Packing/PackingTemplateOption.tsx new file mode 100644 index 000000000..2b9cd6e75 --- /dev/null +++ b/client/src/components/Packing/PackingTemplateOption.tsx @@ -0,0 +1,46 @@ +import { Package } from 'lucide-react'; +import type { TranslationFn } from '../../types'; + +interface PackingTemplateOptionProps { + name: string; + itemCount: number; + onClick: () => void; + t: TranslationFn; + /** Fade the hover background in rather than switching it. */ + fade?: boolean; +} + +/** One packing template in an apply menu: its name over how many items it adds. */ +export function PackingTemplateOption({ name, itemCount, onClick, t, fade = false }: PackingTemplateOptionProps) { + return ( + (e.currentTarget.style.background = 'var(--bg-tertiary)')} + onMouseLeave={(e) => (e.currentTarget.style.background = 'transparent')} + > + + + {name} + + {itemCount} {t('admin.packingTemplates.items', { count: itemCount })} + + + + ); +}