Skip to content

Commit 0b71717

Browse files
authored
perf(icons): reduce and guard SVG path precision (#6839)
* perf(react): reduce SVG path precision * fix(react): preserve Sim wordmark precision * fix(icons): preserve Quartr scale * test(icons): ratchet SVG path precision * fix(icons): make precision exceptions local * perf(icons): enforce three-decimal paths
1 parent 2ced737 commit 0b71717

11 files changed

Lines changed: 789 additions & 285 deletions

File tree

apps/docs/components/icons.tsx

Lines changed: 133 additions & 139 deletions
Large diffs are not rendered by default.

apps/sim/components/icons.tsx

Lines changed: 133 additions & 139 deletions
Large diffs are not rendered by default.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@
1414
"dev:sockets": "cd apps/realtime && bun run dev",
1515
"dev:full": "bunx concurrently -n \"App,Realtime\" -c \"cyan,magenta\" \"cd apps/sim && bun run dev\" \"cd apps/realtime && bun run dev\"",
1616
"dev:full:capped": "bunx concurrently -n \"App,Realtime\" -c \"cyan,magenta\" \"cd apps/sim && bun run dev:capped\" \"cd apps/realtime && bun run dev\"",
17-
"test": "bun run test:setup && turbo run test",
17+
"test": "bun run test:setup && bun run test:icon-path-precision && turbo run test",
1818
"test:setup": "bun run --cwd packages/sim-setup test",
19+
"test:icon-path-precision": "bunx vitest run scripts/check-icon-path-precision.test.ts",
1920
"format": "turbo run format",
2021
"format:check": "turbo run format:check",
2122
"lint": "turbo run lint",
@@ -47,6 +48,7 @@
4748
"check:canvas-sentences": "bun run apps/sim/scripts/check-canvas-sentences.ts --require-coverage",
4849
"check:bare-icons": "bun run scripts/check-bare-icons.ts",
4950
"check:icon-paths": "bun run scripts/check-icon-paths.ts",
51+
"check:icon-path-precision": "bun run scripts/check-icon-path-precision.ts",
5052
"check:migrations": "bun run scripts/check-migrations-safety.ts",
5153
"check:native-typecheck": "bun run scripts/check-native-typecheck.ts",
5254
"check:source-text": "bun run scripts/check-source-text.ts",

packages/emcn/src/icons/bubble-chat-delay.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export function BubbleChatDelay({ size = 24, width, height, ...props }: IconProp
2121
{...props}
2222
>
2323
<path
24-
d='M21.5 12C21.5 17.2467 17.2467 21.5 12 21.5C10.3719 21.5 8.8394 21.0904 7.5 20.3687C5.63177 19.362 4.37462 20.2979 3.26592 20.4658C3.09774 20.4913 2.93024 20.4302 2.80997 20.31C2.62741 20.1274 2.59266 19.8451 2.6935 19.6074C3.12865 18.5818 3.5282 16.6382 2.98341 15C2.6698 14.057 2.5 13.0483 2.5 12C2.5 6.75329 6.75329 2.5 12 2.5C17.2467 2.5 21.5 6.75329 21.5 12Z'
24+
d='M21.5 12C21.5 17.25 17.25 21.5 12 21.5C10.37 21.5 8.84 21.09 7.5 20.37C5.63 19.36 4.37 20.3 3.27 20.47C3.1 20.49 2.93 20.43 2.81 20.31C2.63 20.13 2.59 19.85 2.69 19.61C3.13 18.58 3.53 16.64 2.98 15C2.67 14.06 2.5 13.05 2.5 12C2.5 6.75 6.75 2.5 12 2.5C17.25 2.5 21.5 6.75 21.5 12Z'
2525
stroke='currentColor'
2626
strokeLinecap='round'
2727
strokeLinejoin='round'

packages/emcn/src/icons/document-attachment.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export function DocumentAttachment(props: SVGProps<SVGSVGElement>) {
1616
{...props}
1717
>
1818
<path
19-
d='M10.5 5.69V5.1C10.5 3.99 10.499 3.2 10.414 2.61C10.33 2.02 10.18 1.69 9.91 1.44C9.65 1.19 9.28 1.04 8.65 0.96C8.01 0.88 7.17 0.875 6 0.875H5.38C4.2 0.875 3.36 0.88 2.72 0.96C2.09 1.04 1.73 1.19 1.46 1.44C1.2 1.69 1.04 2.02 0.96 2.61C0.88 3.2 0.88 3.99 0.88 5.1V7.44C0.88 8.55 0.88 9.34 0.96 9.94C1.04 10.52 1.2 10.85 1.46 11.1C1.73 11.354 2.09 11.5 2.72 11.58C3.36 11.67 4.2 11.67 5.38 11.67H5.69C5.93 11.67 6.125 11.86 6.125 12.1C6.125 12.35 5.93 12.54 5.69 12.54H5.38C4.23 12.54 3.32 12.54 2.61 12.45C1.9 12.36 1.32 12.17 0.86 11.739C0.4 11.3 0.19 10.75 0.09 10.06C0 9.39 8.49e-07 8.52 8.99e-07 7.44V5.1C9.49e-07 4.02 0 3.16 0.09 2.48C0.19 1.79 0.4 1.24 0.86 0.8C1.32 0.37 1.9 0.18 2.61 0.09C3.32 0 4.23 2.75e-07 5.38 3.23e-07H6C7.15 3.71e-07 8.06 0 8.76 0.09C9.48 0.18 10.06 0.37 10.51 0.8C10.98 1.24 11.18 1.79 11.28 2.48C11.38 3.16 11.375 4.02 11.375 5.1V5.69C11.375 5.93 11.18 6.125 10.94 6.125C10.7 6.125 10.5 5.93 10.5 5.69Z'
19+
d='M10.5 5.69V5.1C10.5 3.99 10.499 3.2 10.414 2.61C10.33 2.02 10.18 1.69 9.91 1.44C9.65 1.19 9.28 1.04 8.65 0.96C8.01 0.88 7.17 0.875 6 0.875H5.38C4.2 0.875 3.36 0.88 2.72 0.96C2.09 1.04 1.73 1.19 1.46 1.44C1.2 1.69 1.04 2.02 0.96 2.61C0.88 3.2 0.88 3.99 0.88 5.1V7.44C0.88 8.55 0.88 9.34 0.96 9.94C1.04 10.52 1.2 10.85 1.46 11.1C1.73 11.354 2.09 11.5 2.72 11.58C3.36 11.67 4.2 11.67 5.38 11.67H5.69C5.93 11.67 6.125 11.86 6.125 12.1C6.125 12.35 5.93 12.54 5.69 12.54H5.38C4.23 12.54 3.32 12.54 2.61 12.45C1.9 12.36 1.32 12.17 0.86 11.739C0.4 11.3 0.19 10.75 0.09 10.06C0 9.39 0 8.52 0 7.44V5.1C0 4.02 0 3.16 0.09 2.48C0.19 1.79 0.4 1.24 0.86 0.8C1.32 0.37 1.9 0.18 2.61 0.09C3.32 0 4.23 0 5.38 0H6C7.15 0 8.06 0 8.76 0.09C9.48 0.18 10.06 0.37 10.51 0.8C10.98 1.24 11.18 1.79 11.28 2.48C11.38 3.16 11.375 4.02 11.375 5.1V5.69C11.375 5.93 11.18 6.125 10.94 6.125C10.7 6.125 10.5 5.93 10.5 5.69Z'
2020
fill='currentColor'
2121
/>
2222
<path

packages/emcn/src/icons/download.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export function Download({ animate = false, className, ...props }: DownloadProps
3333
{...props}
3434
>
3535
{/* tray — same as Upload */}
36-
<path d='M0.75 12.75V16.75C0.75 17.8546 1.64543 18.75 2.75 18.75H17.75C18.8546 18.75 19.75 17.8546 19.75 16.75V12.75' />
36+
<path d='M0.75 12.75V16.75C0.75 17.85 1.65 18.75 2.75 18.75H17.75C18.85 18.75 19.75 17.85 19.75 16.75V12.75' />
3737
{/* stem — top to tray */}
3838
<path d='M10.25 1.75V12.75' />
3939
{/* arrowhead pointing down */}

packages/emcn/src/icons/duplicate.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export function Duplicate(props: SVGProps<SVGSVGElement>) {
1919
aria-hidden='true'
2020
{...props}
2121
>
22-
<path d='M14.25 0.75H2.75C1.64543 0.75 0.75 1.64543 0.75 2.75V14.25' />
22+
<path d='M14.25 0.75H2.75C1.65 0.75 0.75 1.65 0.75 2.75V14.25' />
2323
<rect x='5.25' y='5.25' width='14' height='14' rx='2' />
2424
</svg>
2525
)

packages/emcn/src/icons/mic.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export function Mic(props: SVGProps<SVGSVGElement>) {
2020
{...props}
2121
>
2222
<rect x='7.25' y='0.75' width='6' height='12' rx='3' />
23-
<path d='M3.75 9.25V10.25C3.75 13.8399 6.66015 16.75 10.25 16.75C13.8399 16.75 16.75 13.8399 16.75 10.25V9.25' />
23+
<path d='M3.75 9.25V10.25C3.75 13.84 6.66 16.75 10.25 16.75C13.84 16.75 16.75 13.84 16.75 10.25V9.25' />
2424
<path d='M10.25 16.75V19.25' />
2525
</svg>
2626
)

packages/emcn/src/icons/upload.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export function Upload(props: SVGProps<SVGSVGElement>) {
1919
aria-hidden='true'
2020
{...props}
2121
>
22-
<path d='M0.75 12.75V16.75C0.75 17.8546 1.64543 18.75 2.75 18.75H17.75C18.8546 18.75 19.75 17.8546 19.75 16.75V12.75' />
22+
<path d='M0.75 12.75V16.75C0.75 17.85 1.65 18.75 2.75 18.75H17.75C18.85 18.75 19.75 17.85 19.75 16.75V12.75' />
2323
<path d='M10.25 14.75V1.75' />
2424
<path d='M5.25 6.75L10.25 1.75L15.25 6.75' />
2525
</svg>
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { describe, expect, it } from 'vitest'
2+
import {
3+
analyzeIconSource,
4+
effectiveFractionDigits,
5+
findPrecisionCandidates,
6+
} from './check-icon-path-precision'
7+
8+
const FIXTURE_PATH = '/repo/packages/emcn/src/icons/fixture.tsx'
9+
10+
describe('icon path precision audit', () => {
11+
it('accepts the three-decimal boundary and ignores geometry outside literal paths', () => {
12+
const source = `
13+
const dynamicPath = 'M0.1234 1'
14+
const unrelated = "d='M0.1234 1'"
15+
export function SafeIcon() {
16+
return (
17+
<svg viewBox='0 0 10.1234 10' transform='scale(0.16624)'>
18+
<path d='M.5 1.200L1.2e1 2' />
19+
<path d={dynamicPath} />
20+
</svg>
21+
)
22+
}
23+
`
24+
25+
expect(analyzeIconSource(source, FIXTURE_PATH)).toEqual({
26+
candidates: [],
27+
invalidExceptions: [],
28+
})
29+
})
30+
31+
it('finds ordinary decimals and exponents finer than a thousandth', () => {
32+
const source = `
33+
export function PreciseIcon() {
34+
return <svg><path d='M0.1234 1e-4L2.345 5' /></svg>
35+
}
36+
`
37+
38+
const candidates = findPrecisionCandidates(source, FIXTURE_PATH)
39+
expect(candidates).toHaveLength(1)
40+
expect(candidates[0]).toMatchObject({
41+
icon: 'PreciseIcon',
42+
maxFractionDigits: 4,
43+
offendingNumbers: ['0.1234', '1e-4'],
44+
})
45+
expect(effectiveFractionDigits('2.13949e-05')).toBe(10)
46+
})
47+
48+
it('checks literal JSX expressions and static template literals', () => {
49+
const source = `
50+
export function ExpressionIcon() {
51+
return (
52+
<svg>
53+
<path d={'M0.1234 1'} />
54+
<path d={\`M2.3456 3\`} />
55+
</svg>
56+
)
57+
}
58+
`
59+
60+
expect(findPrecisionCandidates(source, FIXTURE_PATH)).toHaveLength(2)
61+
})
62+
63+
it('accepts a reasoned TSDoc exception on the immediately following path', () => {
64+
const source = `
65+
export function PreciseBrandIcon() {
66+
return (
67+
<svg>
68+
{/**
69+
* svg-path-precision-exception: Rounding visibly distorts the provider-authored mark.
70+
*/}
71+
<path d='M0.1234 1' />
72+
</svg>
73+
)
74+
}
75+
`
76+
77+
expect(analyzeIconSource(source, FIXTURE_PATH)).toEqual({
78+
candidates: [],
79+
invalidExceptions: [],
80+
})
81+
})
82+
83+
it('rejects exceptions without a reason and exceptions on already-clean paths', () => {
84+
const missingReason = `
85+
export function PreciseIcon() {
86+
return <svg>{/** svg-path-precision-exception: */}<path d='M0.1234 1' /></svg>
87+
}
88+
`
89+
const unnecessary = `
90+
export function CleanIcon() {
91+
return <svg>{/** svg-path-precision-exception: Keep detail. */}<path d='M0.12 1' /></svg>
92+
}
93+
`
94+
95+
const missingReasonAnalysis = analyzeIconSource(missingReason, FIXTURE_PATH)
96+
expect(missingReasonAnalysis.candidates).toHaveLength(1)
97+
expect(missingReasonAnalysis.invalidExceptions[0]?.message).toContain('specific reason')
98+
99+
const unnecessaryAnalysis = analyzeIconSource(unnecessary, FIXTURE_PATH)
100+
expect(unnecessaryAnalysis.candidates).toEqual([])
101+
expect(unnecessaryAnalysis.invalidExceptions[0]?.message).toContain('unnecessary')
102+
})
103+
104+
it('requires a reasoned exception to remain while its precise path remains', () => {
105+
const excepted = `
106+
export function PreciseBrandIcon() {
107+
return (
108+
<svg>
109+
{/** svg-path-precision-exception: Rounding visibly distorts the brand mark. */}
110+
<path d='M0.1234 1' />
111+
</svg>
112+
)
113+
}
114+
`
115+
const exceptionRemoved = excepted.replace(
116+
'{/** svg-path-precision-exception: Rounding visibly distorts the brand mark. */}',
117+
''
118+
)
119+
120+
expect(findPrecisionCandidates(excepted, FIXTURE_PATH)).toEqual([])
121+
expect(findPrecisionCandidates(exceptionRemoved, FIXTURE_PATH)).toHaveLength(1)
122+
})
123+
})

0 commit comments

Comments
 (0)