Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 40 additions & 1 deletion src/__tests__/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ describe('propsToPropsWithTypography', () => {
figma.getStyleByIdAsync = (id: string) =>
Promise.resolve(
id === 'ts-1'
? ({ id: 'ts-1', name: 'Typography/Body' } as unknown as BaseStyle)
? ({ id: 'ts-1', name: 'mobile/Body' } as unknown as BaseStyle)
: null,
) as ReturnType<typeof figma.getStyleByIdAsync>

Expand All @@ -63,6 +63,45 @@ describe('propsToPropsWithTypography', () => {
figma.getStyleByIdAsync = origGetStyle
})

it('should preserve scoped (non-breakpoint) prefix in typography key', async () => {
const origGetLocal = figma.getLocalTextStylesAsync
const origGetStyle = figma.getStyleByIdAsync
figma.getLocalTextStylesAsync = () =>
Promise.resolve([{ id: 'ts-cms' } as unknown as TextStyle]) as ReturnType<
typeof figma.getLocalTextStylesAsync
>
figma.getStyleByIdAsync = (id: string) =>
Promise.resolve(
id === 'ts-cms'
? ({
id: 'ts-cms',
name: 'cms/bodyLgBold',
} as unknown as BaseStyle)
: null,
) as ReturnType<typeof figma.getStyleByIdAsync>

// Async path — "cms/" is not a breakpoint, so the full name must be
// converted to camelCase to match the devup.json export key.
const r1 = await propsToPropsWithTypography(
{ fontFamily: 'Arial', fontSize: 18 },
'ts-cms',
)
expect(r1.typography).toBe('cmsBodyLgBold')
expect(r1.fontFamily).toBeUndefined()
expect(r1.fontSize).toBeUndefined()

// Sync fast path — same assertion via the resolved cache branch.
const r2 = await propsToPropsWithTypography(
{ fontFamily: 'Inter', fontSize: 20 },
'ts-cms',
)
expect(r2.typography).toBe('cmsBodyLgBold')
expect(r2.fontFamily).toBeUndefined()

figma.getLocalTextStylesAsync = origGetLocal
figma.getStyleByIdAsync = origGetStyle
})

it('should return early from sync path when textStyleId not in resolved set', async () => {
const origGetLocal = figma.getLocalTextStylesAsync
const origGetStyle = figma.getStyleByIdAsync
Expand Down
2 changes: 1 addition & 1 deletion src/codegen/__tests__/__snapshots__/codegen.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -637,7 +637,7 @@ exports[`Codegen renders text node with TRUNCATE auto resize and fixed sizing 1`
`;

exports[`Codegen renders text node with textStyleId and typography 1`] = `
"<Text boxSize="100%" color="#F00" typography="heading">
"<Text boxSize="100%" color="#F00" typography="typographyHeading">
Heading
</Text>"
`;
Expand Down
9 changes: 6 additions & 3 deletions src/codegen/props/effect.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { optimizeHex } from '../../utils/optimize-hex'
import { rgbaToHex } from '../../utils/rgba-to-hex'
import { styleNameToTypography } from '../../utils/style-name-to-typography'
import { toCamel } from '../../utils/to-camel'
import { addPx } from '../utils/add-px'
import { getVariableByIdCached } from '../utils/variable-cache'
Expand All @@ -9,6 +10,10 @@ type BoundVars = Record<string, { id: string } | undefined> | undefined
/**
* Resolve effectStyleId to a `$token` for the entire shadow value.
* The effect style name IS the shadow token (not a color token).
*
* Must match the key written by export-devup.ts via styleNameToTypography,
* so only breakpoint prefixes are stripped. Scoped prefixes (e.g. "cms/xyz")
* stay part of the token name.
*/
async function _resolveEffectStyleToken(
node: SceneNode,
Expand All @@ -18,9 +23,7 @@ async function _resolveEffectStyleToken(
if (!styleId || typeof styleId !== 'string') return null
const style = await figma.getStyleByIdAsync(styleId)
if (style?.name) {
// Strip responsive level prefix (e.g. "3/testShadow" → "testShadow")
const parts = style.name.split('/')
return `$${toCamel(parts[parts.length - 1])}`
return `$${styleNameToTypography(style.name).name}`
}
return null
}
Expand Down
11 changes: 9 additions & 2 deletions src/codegen/props/text-shadow.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import { optimizeHex } from '../../utils/optimize-hex'
import { rgbaToHex } from '../../utils/rgba-to-hex'
import { styleNameToTypography } from '../../utils/style-name-to-typography'
import { toCamel } from '../../utils/to-camel'
import { addPx } from '../utils/add-px'
import { getVariableByIdCached } from '../utils/variable-cache'

type BoundVars = Record<string, { id: string } | undefined> | undefined

/**
* Resolve effectStyleId to a `$token` for the text shadow value.
*
* Must match the key written by export-devup.ts via styleNameToTypography,
* so only breakpoint prefixes are stripped. Scoped prefixes (e.g. "cms/xyz")
* stay part of the token name.
*/
async function _resolveEffectStyleToken(
node: SceneNode,
): Promise<string | null> {
Expand All @@ -14,8 +22,7 @@ async function _resolveEffectStyleToken(
if (!styleId || typeof styleId !== 'string') return null
const style = await figma.getStyleByIdAsync(styleId)
if (style?.name) {
const parts = style.name.split('/')
return `$${toCamel(parts[parts.length - 1])}`
return `$${styleNameToTypography(style.name).name}`
}
return null
}
Expand Down
9 changes: 6 additions & 3 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { toCamel } from './utils/to-camel'
import { styleNameToTypography } from './utils/style-name-to-typography'
import { toPascal } from './utils/to-pascal'

// Cache for figma.getStyleByIdAsync() — keyed by style ID
Expand Down Expand Up @@ -27,8 +27,11 @@ function applyTypographyStyle(
ret: Record<string, unknown>,
style: BaseStyle,
): void {
const split = style.name.split('/')
ret.typography = toCamel(split[split.length - 1])
// Must match the key that export-devup.ts writes via styleNameToTypography,
// otherwise `typography="..."` references miss the exported devup.json key.
// Only breakpoint prefixes (mobile/tablet/desktop/{number}) are stripped;
// scoped prefixes like `cms/bodyLgBold` stay intact → `cmsBodyLgBold`.
ret.typography = styleNameToTypography(style.name).name
delete ret.fontFamily
delete ret.fontSize
delete ret.fontWeight
Expand Down