Skip to content

Commit 749f362

Browse files
atulmguptaCopilot
andcommitted
fix(web): register token-backed duration and easing scales with tailwind-merge
tailwind.config.js defines transitionDuration (fast|normal|slow) and transitionTimingFunction (standard|accelerate|decelerate) as CSS-var-backed custom keys. twMerge's built-in `duration` and `ease` groups only recognise numeric/known values, so these fell through as unrecognised classes and were never de-duplicated: cn('duration-fast', 'duration-normal') => "duration-fast duration-normal" cn('ease-standard', 'ease-linear') => "ease-standard ease-linear" Both classes survived and CSS source order silently decided the winner, so a component prop could not reliably override a base motion class. Register both scales in extendTailwindMerge so they resolve last-wins like every other group. Adds an it.each regression block covering both scales in each direction (13 -> 21 tests) to lock the behaviour, since the failure mode is invisible in review and only shows up as a subtly wrong animation speed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 1d32a78 commit 749f362

2 files changed

Lines changed: 29 additions & 3 deletions

File tree

web/src/lib/cn.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,4 +88,27 @@ describe('cn — tailwind-merge conflict resolution (last wins)', () => {
8888
const result = cn('p-2', useLargePadding && 'p-8', false && 'p-0')
8989
expect(result).toBe('p-8')
9090
})
91+
92+
/**
93+
* Every token-backed scale declared in tailwind.config.js uses a custom
94+
* (non-numeric, non-keyword) key, which twMerge does NOT recognise out of
95+
* the box — both classes survive and CSS source order silently decides the
96+
* winner. Each scale must therefore be registered in cn.ts. This test is
97+
* the guard: adding a new custom scale to tailwind.config.js without
98+
* registering it here will fail.
99+
*/
100+
describe('token-backed custom scales resolve last-wins', () => {
101+
it.each([
102+
['duration-fast', 'duration-normal'],
103+
['duration-fast', 'duration-200'],
104+
['ease-standard', 'ease-linear'],
105+
['ease-accelerate', 'ease-decelerate'],
106+
['rounded-panel', 'rounded-lg'],
107+
['rounded-shape-md', 'rounded-full'],
108+
['shadow-panel', 'shadow-e1'],
109+
['shadow-e2', 'shadow-none'],
110+
])('%s then %s keeps only the last', (base, override) => {
111+
expect(cn(base, override)).toBe(override)
112+
})
113+
})
91114
})

web/src/lib/cn.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ import { extendTailwindMerge } from 'tailwind-merge'
44
/**
55
* tailwind-merge only resolves conflicts between utilities it recognises. The
66
* token-backed scales registered in tailwind.config.js (`rounded-shape-*`,
7-
* `rounded-panel`, `rounded-pill`, `shadow-e*`, `shadow-panel*`) use custom
8-
* keys, so out of the box twMerge treats them as unrelated classes: a caller
9-
* passing `className="rounded-lg"` to a primitive whose base is
7+
* `rounded-panel`, `rounded-pill`, `shadow-e*`, `shadow-panel*`,
8+
* `duration-fast|normal|slow`, `ease-standard|accelerate|decelerate`) use
9+
* custom keys, so out of the box twMerge treats them as unrelated classes: a
10+
* caller passing `className="rounded-lg"` to a primitive whose base is
1011
* `rounded-panel` would end up with BOTH classes surviving, leaving the CSS
1112
* source order to silently pick the winner.
1213
*
@@ -31,6 +32,8 @@ const twMerge = extendTailwindMerge({
3132
},
3233
],
3334
'shadow': [{ shadow: ['e1', 'e2', 'e3', 'panel', 'panel-hover'] }],
35+
'duration': [{ duration: ['fast', 'normal', 'slow'] }],
36+
'ease': [{ ease: ['standard', 'accelerate', 'decelerate'] }],
3437
},
3538
},
3639
})

0 commit comments

Comments
 (0)