mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-20 05:56:53 +00:00
fix(dayplan): the delete question asks inside the day dialog, and its buttons stay small
Deleting a day no longer stacks a second dialog over the reorder dialog. The dialog swaps its list for the question at the same size and in the same place; Cancel and Escape bring the list back with the focus on the row asked about. The foot is one row of normal sized buttons with the hint line above it, the undated day is labelled "Without date" in every language, and the dialog is a step wider so no label breaks. The list of what goes with a day is quieter: a cancelled stay tints its whole row, a day with nothing on it is one plain line. On the phone the add buttons are two pills under the scrolling list, and the confirm sheet moves a long label to its own row instead of breaking it.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
// FE-COMP-DAYIMPACT-001 to FE-COMP-DAYIMPACT-004
|
||||
// FE-COMP-DAYIMPACT-001 to FE-COMP-DAYIMPACT-006
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { BedDouble, MapPin } from 'lucide-react'
|
||||
import { BedDouble, CalendarCheck, MapPin } from 'lucide-react'
|
||||
import { render, screen } from '../../../tests/helpers/render'
|
||||
import DayImpactList from './DayImpactList'
|
||||
import MDayImpactList from '../../mobile/components/MDayImpactList'
|
||||
@@ -23,12 +23,16 @@ describe('DayImpactList', () => {
|
||||
expect(rows[1]).toHaveTextContent('Planned places: 3')
|
||||
})
|
||||
|
||||
it('FE-COMP-DAYIMPACT-002: the tone reaches the row: money in the danger tokens, the rest in the content tokens', () => {
|
||||
it('FE-COMP-DAYIMPACT-002: the tone reaches the row: money tints the whole row in the danger tokens, the rest stays in the content tokens', () => {
|
||||
render(<DayImpactList lines={lines} />)
|
||||
const [stay, places] = screen.getAllByRole('listitem')
|
||||
expect(stay).toHaveAttribute('data-tone', 'danger')
|
||||
expect(stay).toHaveClass('bg-danger-soft')
|
||||
expect(screen.getByText('Stay at Harbour Hotel')).toHaveClass('text-danger')
|
||||
expect(stay.querySelector('span')).toHaveClass('bg-danger-soft')
|
||||
// The chip stands out on the tinted row instead of melting into it.
|
||||
expect(stay.querySelector('span')).toHaveClass('text-danger')
|
||||
expect(stay.querySelector('span')).not.toHaveClass('bg-danger-soft')
|
||||
expect(places).not.toHaveClass('bg-danger-soft')
|
||||
expect(screen.getByText('Planned places: 3')).toHaveClass('text-content')
|
||||
expect(places.querySelectorAll('p')).toHaveLength(1)
|
||||
})
|
||||
@@ -63,4 +67,28 @@ describe('DayImpactList', () => {
|
||||
// The first content row sits under the day chips, so it gets the hairline.
|
||||
expect(phoneRows[1].className).toContain('border-t')
|
||||
})
|
||||
it('FE-COMP-DAYIMPACT-005: a day with nothing on it is one plain line, without the card and its chips', () => {
|
||||
const empty: ImpactLine[] = [{ key: 'empty', icon: CalendarCheck, tone: 'muted', text: 'Nothing is planned on this day.' }]
|
||||
render(<DayImpactList lines={empty} label="desk" />)
|
||||
const list = screen.getByRole('list', { name: 'desk' })
|
||||
const rows = list.querySelectorAll('li')
|
||||
expect(rows).toHaveLength(1)
|
||||
expect(rows[0]).toHaveAttribute('data-kind', 'empty')
|
||||
expect(rows[0]).toHaveTextContent('Nothing is planned on this day.')
|
||||
expect(list).not.toHaveClass('divide-y')
|
||||
expect(rows[0].querySelectorAll('p')).toHaveLength(0)
|
||||
|
||||
// The same quiet line on the phone, and a muted line among others keeps its row.
|
||||
render(<MDayImpactList lines={empty} label="phone" />)
|
||||
expect(screen.getByRole('list', { name: 'phone' }).querySelector('li')?.className).toContain('var(--m-inner)')
|
||||
render(<DayImpactList lines={[...lines, empty[0]]} label="mixed" />)
|
||||
expect(screen.getByRole('list', { name: 'mixed' }).querySelectorAll('li')).toHaveLength(3)
|
||||
})
|
||||
|
||||
it('FE-COMP-DAYIMPACT-006: on the phone the cancelled stay is tinted as a whole row too', () => {
|
||||
render(<MDayImpactList lines={lines} label="phone" />)
|
||||
const [stay, places] = Array.from(screen.getByRole('list', { name: 'phone' }).querySelectorAll('li'))
|
||||
expect(stay.className).toContain('var(--m-st-danger)')
|
||||
expect(places.className).not.toContain('var(--m-st-danger)')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -15,24 +15,33 @@ export interface ImpactListSkin {
|
||||
hint: string
|
||||
chipTone: Record<ImpactTone, string>
|
||||
textTone: Record<ImpactTone, string>
|
||||
/** The whole row per tone: a cancelled stay is tinted from edge to edge, not only in its text. */
|
||||
rowTone: Record<ImpactTone, string>
|
||||
/** A day with nothing on it: one quiet line instead of a card. */
|
||||
plain: string
|
||||
plainIcon: string
|
||||
}
|
||||
|
||||
const DESKTOP: ImpactListSkin = {
|
||||
list: 'mt-4 overflow-hidden rounded-xl border border-edge-faint bg-surface-secondary divide-y divide-edge-faint',
|
||||
row: 'flex items-start gap-3 px-3 py-2.5',
|
||||
list: 'mt-3 overflow-hidden rounded-xl border border-edge-faint divide-y divide-edge-faint',
|
||||
row: 'flex items-start gap-2.5 px-3 py-2',
|
||||
divider: '',
|
||||
chip: 'mt-0.5 flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-lg',
|
||||
dayChip: 'rounded-full border border-edge-faint bg-surface px-2.5 py-0.5 text-caption font-medium text-content-secondary',
|
||||
iconSize: 15,
|
||||
text: 'text-body font-medium leading-snug',
|
||||
hint: 'mt-0.5 text-caption text-content-muted',
|
||||
chip: 'flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-lg',
|
||||
dayChip: 'rounded-full border border-edge-faint bg-surface-secondary px-2 py-px text-caption font-medium text-content-secondary',
|
||||
iconSize: 14,
|
||||
text: 'text-body font-medium leading-snug text-pretty',
|
||||
hint: 'mt-px text-caption leading-snug text-content-muted text-pretty',
|
||||
chipTone: {
|
||||
neutral: 'bg-surface text-content-muted',
|
||||
muted: 'bg-surface text-content-faint',
|
||||
neutral: 'bg-surface-secondary text-content-muted',
|
||||
muted: 'bg-surface-secondary text-content-faint',
|
||||
warning: 'bg-warning-soft text-warning',
|
||||
danger: 'bg-danger-soft text-danger',
|
||||
// The row itself is tinted already; a card-coloured chip stands out on it in both themes.
|
||||
danger: 'bg-surface-card text-danger',
|
||||
},
|
||||
textTone: { neutral: 'text-content', muted: 'text-content-muted', warning: 'text-content', danger: 'text-danger' },
|
||||
rowTone: { neutral: '', muted: '', warning: '', danger: 'bg-danger-soft' },
|
||||
plain: 'mt-3 flex items-center gap-2 rounded-xl bg-surface-secondary px-3 py-2.5 text-body text-content-muted',
|
||||
plainIcon: 'flex-shrink-0 text-content-faint',
|
||||
}
|
||||
|
||||
interface DayImpactListProps {
|
||||
@@ -53,11 +62,22 @@ interface DayImpactListProps {
|
||||
* chip in the row's tone, the count, and a caption saying what happens to it.
|
||||
* The rows come ready made from utils/dayImpactLines, and the look comes from a
|
||||
* skin, so the desktop dialog and the phone sheet share this markup and differ
|
||||
* only in their tokens.
|
||||
* only in their tokens. A day with nothing on it gets one plain line.
|
||||
*/
|
||||
export default function DayImpactList({ lines, days = [], label, skin = DESKTOP }: DayImpactListProps) {
|
||||
if (lines.length === 0) return null
|
||||
const offset = days.length > 0 ? 1 : 0
|
||||
if (offset === 0 && lines.length === 1 && lines[0].tone === 'muted') {
|
||||
const { key, icon: Icon, text } = lines[0]
|
||||
return (
|
||||
<ul aria-label={label} className="m-0 list-none p-0">
|
||||
<li data-tone="muted" data-kind={key} className={skin.plain}>
|
||||
<Icon size={skin.iconSize + 1} strokeWidth={2} aria-hidden="true" className={skin.plainIcon} />
|
||||
<span className="min-w-0">{text}</span>
|
||||
</li>
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<ul aria-label={label} className={skin.list}>
|
||||
{offset > 0 && (
|
||||
@@ -65,17 +85,17 @@ export default function DayImpactList({ lines, days = [], label, skin = DESKTOP
|
||||
<span className={`${skin.chip} ${skin.chipTone.warning}`}>
|
||||
<CalendarMinus size={skin.iconSize} strokeWidth={2} aria-hidden="true" />
|
||||
</span>
|
||||
<div className="flex min-w-0 flex-1 flex-wrap gap-1.5 pt-1">
|
||||
<div className="flex min-w-0 flex-1 flex-wrap items-center gap-1 self-center">
|
||||
{days.map((name, i) => <span key={`${i}-${name}`} className={skin.dayChip}>{name}</span>)}
|
||||
</div>
|
||||
</li>
|
||||
)}
|
||||
{lines.map(({ key, icon: Icon, text, hint, tone }, i) => (
|
||||
<li key={key} data-tone={tone} className={`${skin.row} ${i + offset > 0 ? skin.divider : ''}`.trim()}>
|
||||
<li key={key} data-tone={tone} className={`${skin.row} ${skin.rowTone[tone]} ${i + offset > 0 ? skin.divider : ''}`.trim()}>
|
||||
<span className={`${skin.chip} ${skin.chipTone[tone]}`}>
|
||||
<Icon size={skin.iconSize} strokeWidth={2} aria-hidden="true" />
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="min-w-0 flex-1 self-center">
|
||||
<p className={`${skin.text} ${skin.textTone[tone]}`}>{text}</p>
|
||||
{hint && <p className={skin.hint}>{hint}</p>}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user