Skip to content

Commit 020be86

Browse files
committed
chore: update packages-runtime/variants
1 parent c9baaa5 commit 020be86

File tree

2 files changed

+151
-17
lines changed

2 files changed

+151
-17
lines changed

packages-runtime/variants/src/index.ts

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,13 @@ import {
1818

1919
type TailwindVariantsCn = <T extends CnOptions>(...classes: T) => (config?: TWMConfig) => CnReturn
2020

21-
type TailwindVariantsComponent = ReturnType<typeof tailwindVariantsTv>
21+
type TailwindVariantsComponent = ((props?: unknown) => unknown) & Record<PropertyKey, unknown>
2222

23-
type TailwindVariantsResult = ReturnType<TailwindVariantsComponent>
23+
type TailwindVariantsResult<TComponent extends TailwindVariantsComponent> = ReturnType<TComponent>
24+
25+
type TailwindVariantsOptions = Parameters<typeof tailwindVariantsTv>[0]
26+
27+
type TailwindVariantsConfig = Parameters<typeof tailwindVariantsTv>[1]
2428

2529
function mergeConfigs(...configs: (TVConfig | undefined)[]): TVConfig {
2630
const baseTwMergeConfig = defaultConfig.twMergeConfig
@@ -72,19 +76,19 @@ function copyComponentMetadata(target: TailwindVariantsComponent, source: Tailwi
7276
Object.defineProperties(target, descriptors)
7377
}
7478

75-
function wrapComponent(
76-
component: TailwindVariantsComponent,
79+
function wrapComponent<TComponent extends TailwindVariantsComponent>(
80+
component: TComponent,
7781
config: TVConfig,
7882
mergeClassList: (value: CnReturn, config?: TWMConfig) => CnReturn,
79-
): TailwindVariantsComponent {
83+
): TComponent {
8084
const wrapped = ((props?: unknown) => {
81-
const result = component(props) as TailwindVariantsResult
85+
const result = component(props) as TailwindVariantsResult<TComponent>
8286

8387
if (result == null || typeof result === 'string') {
8488
return mergeClassList(result as CnReturn, config)
8589
}
8690

87-
const slotEntries = Reflect.ownKeys(result) as Array<keyof typeof result>
91+
const slotEntries = Reflect.ownKeys(result) as Array<keyof TailwindVariantsResult<TComponent>>
8892
const slots: Record<PropertyKey, any> = {}
8993

9094
for (const key of slotEntries) {
@@ -99,8 +103,8 @@ function wrapComponent(
99103
}
100104
}
101105

102-
return slots as typeof result
103-
}) as TailwindVariantsComponent
106+
return slots as TailwindVariantsResult<TComponent>
107+
}) as TComponent
104108

105109
copyComponentMetadata(wrapped, component)
106110

@@ -160,23 +164,29 @@ function createVariantsRuntime(options?: CreateOptions) {
160164
return transformers.escape(normalized)
161165
}
162166

163-
const tv: TV = ((options, config) => {
167+
const tv = ((options: TailwindVariantsOptions, config?: TailwindVariantsConfig) => {
164168
const mergedConfig = mergeConfigs(config)
165169
const upstreamConfig = disableTailwindMerge(config)
166-
const component = tailwindVariantsTv(options, upstreamConfig)
170+
const component = tailwindVariantsTv(
171+
options as TailwindVariantsOptions,
172+
upstreamConfig as TailwindVariantsConfig,
173+
) as unknown as TailwindVariantsComponent
167174
return wrapComponent(component, mergedConfig, mergeClassList)
168-
}) as TV
175+
}) as unknown as TV
169176

170177
const createTVRuntime: typeof tailwindVariantsCreateTV = (configProp?: TVConfig) => {
171178
const tailwindCreate = tailwindVariantsCreateTV(disableTailwindMerge(configProp))
172179

173-
return (options, config) => {
180+
return ((options: TailwindVariantsOptions, config?: TailwindVariantsConfig) => {
174181
const mergedConfig = mergeConfigs(configProp, config)
175-
const component = config
176-
? tailwindCreate(options, disableTailwindMerge(config))
177-
: tailwindCreate(options)
182+
const component = (config
183+
? tailwindCreate(
184+
options as TailwindVariantsOptions,
185+
disableTailwindMerge(config) as TailwindVariantsConfig,
186+
)
187+
: tailwindCreate(options as TailwindVariantsOptions)) as unknown as TailwindVariantsComponent
178188
return wrapComponent(component, mergedConfig, mergeClassList)
179-
}
189+
}) as unknown as ReturnType<typeof tailwindVariantsCreateTV>
180190
}
181191

182192
return {

packages-runtime/variants/test/variants.test.ts

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,63 @@ describe('variants runtime', () => {
3636
expect(button({ tone: 'accent' })).toBe('text-_b_hececec_B')
3737
})
3838

39+
it('escapes rpx units and color literals inside tv output', () => {
40+
const stack = tv({
41+
base: 'text-[24rpx] bg-[#0f0]',
42+
variants: {
43+
tone: {
44+
danger: 'text-[#FF0000] text-[32rpx]',
45+
},
46+
},
47+
})
48+
49+
const baseOutput = stack()?.split(' ')
50+
expect(new Set(baseOutput)).toEqual(new Set(['text-_b24rpx_B', 'bg-_b_h0f0_B']))
51+
52+
const variantOutput = stack({ tone: 'danger' })?.split(' ')
53+
expect(new Set(variantOutput)).toEqual(new Set([
54+
'text-_b32rpx_B',
55+
'bg-_b_h0f0_B',
56+
'text-_b_hFF0000_B',
57+
]))
58+
})
59+
3960
it('supports custom transformers through create', () => {
4061
const { tv: rawTv } = createVariants({ escape: false })
4162
const badge = rawTv({ base: 'text-[#ececec]' })
4263

4364
expect(badge()).toBe('text-[#ececec]')
4465
})
4566

67+
it('respects disabled escape/unescape options in runtime factory', () => {
68+
const runtime = createVariants({ escape: false, unescape: false })
69+
const badge = runtime.tv({
70+
base: 'text-[#ECECEC]',
71+
variants: {
72+
tone: {
73+
accent: 'text-[#101010]',
74+
},
75+
},
76+
})
77+
78+
// Should bypass escape/unescape while still merging conflicting classes.
79+
expect(badge({ tone: 'accent' })).toBe('text-[#101010]')
80+
81+
const aggregate = runtime.cn('text-[#ECECEC]', 'text-[#101010]')
82+
expect(aggregate()).toBe('text-[#101010]')
83+
expect(aggregate({ twMerge: false })).toBe('text-[#ECECEC] text-[#101010]')
84+
85+
const mergedTv = runtime.tv({
86+
base: 'text-[#101010]',
87+
variants: {
88+
tone: {
89+
accent: 'text-[#ECECEC]',
90+
},
91+
},
92+
})
93+
expect(mergedTv({ tone: 'accent' })).toBe('text-[#ECECEC]')
94+
})
95+
4696
it('wraps cn aggregation with merge + escaping', () => {
4797
const aggregate = cn('text-[#ececec]', 'text-[#ECECEC]')
4898

@@ -74,4 +124,78 @@ describe('variants runtime', () => {
74124
expect(aggregate()).toBeUndefined()
75125
expect(cnBase()).toBeUndefined()
76126
})
127+
128+
it('aggregates rpx units and hex colors via cn helpers', () => {
129+
const aggregate = cn('text-[16rpx]', 'bg-[#ff0000]', 'text-[#00FF00]')
130+
131+
expect(aggregate()).toBe('text-_b16rpx_B bg-_b_hff0000_B text-_b_h00FF00_B')
132+
expect(cnBase('border-[2rpx]', 'text-[#123456]')).toBe('border-_b2rpx_B text-_b_h123456_B')
133+
})
134+
135+
it('preserves tailwind-variants metadata when wrapping components', () => {
136+
const badge = tv({
137+
base: 'text-[#ececec]',
138+
variants: {
139+
size: {
140+
sm: 'text-sm',
141+
},
142+
},
143+
defaultVariants: {
144+
size: 'sm',
145+
},
146+
})
147+
148+
expect(badge.variantKeys).toEqual(['size'])
149+
expect(badge.defaultVariants).toEqual({ size: 'sm' })
150+
})
151+
152+
it('wraps slot functions so per-slot output is escaped and merged', () => {
153+
const alert = tv({
154+
slots: {
155+
base: 'text-[#101010]',
156+
icon: 'text-[#202020]',
157+
},
158+
variants: {
159+
tone: {
160+
accent: {
161+
base: 'text-[#ECECEC]',
162+
icon: 'text-[#ECECEC]',
163+
},
164+
},
165+
},
166+
})
167+
168+
const slots = alert({ tone: 'accent' })
169+
expect(typeof slots.base).toBe('function')
170+
expect(slots.base()).toBe('text-_b_hECECEC_B')
171+
expect(slots.icon()).toBe('text-_b_hECECEC_B')
172+
expect(slots.icon({ class: 'text-[#303030]' })).toBe('text-_b_h303030_B')
173+
})
174+
175+
it('merges config overrides from createTV invocations', () => {
176+
const makeTv = createTV({ twMerge: false })
177+
const badge = makeTv({
178+
base: 'text-[#101010]',
179+
variants: {
180+
tone: {
181+
accent: 'text-[#ECECEC]',
182+
},
183+
},
184+
})
185+
186+
expect(badge({ tone: 'accent' })).toBe('text-_b_h101010_B text-_b_hECECEC_B')
187+
188+
const mergedBadge = makeTv(
189+
{
190+
base: 'text-[#101010]',
191+
variants: {
192+
tone: {
193+
accent: 'text-[#ECECEC]',
194+
},
195+
},
196+
},
197+
{ twMerge: true },
198+
)
199+
expect(mergedBadge({ tone: 'accent' })).toBe('text-_b_hECECEC_B')
200+
})
77201
})

0 commit comments

Comments
 (0)