@payfit/unity-illustrations 2.55.21 → 2.55.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/package.json +18 -11
  2. package/skills/unity-illustrations/SKILL.md +39 -0
  3. package/src/agent-references/illustrations-catalog.json +1996 -0
  4. package/src/agent-references/manifest.json +16 -0
  5. package/dist/esm/scripts/agent-references/illustration-reference.d.ts +0 -17
  6. package/src/agent-references/README.mdx +0 -17
  7. package/src/agent-references/illustrations.mdx +0 -74
  8. package/src/components/illustration/Illustration.stories.tsx +0 -362
  9. package/src/components/illustration/Illustration.tsx +0 -169
  10. package/src/components/illustration/figma/IllustrationLarge.figma.ts +0 -258
  11. package/src/components/illustration/figma/IllustrationMedium.figma.ts +0 -258
  12. package/src/components/illustration/figma/IllustrationSmall.figma.ts +0 -258
  13. package/src/components/lazy-illustration/LazyIllustration.stories.tsx +0 -204
  14. package/src/components/lazy-illustration/LazyIllustration.tsx +0 -125
  15. package/src/components/lazy-illustration/figma/LazyIllustrationLarge.figma.ts +0 -255
  16. package/src/components/lazy-illustration/figma/LazyIllustrationMedium.figma.ts +0 -255
  17. package/src/components/lazy-illustration/figma/LazyIllustrationSmall.figma.ts +0 -255
  18. package/src/docs/1-Introduction/1-Welcome.mdx +0 -111
  19. package/src/docs/1-Introduction/2-Getting Started.mdx +0 -25
  20. package/src/docs/1-Introduction/3-Usage.mdx +0 -114
  21. package/src/docs/1-Introduction/4-Changelog.mdx +0 -6
  22. package/src/docs/2-Reference/1-Find my illustration.mdx +0 -12
  23. package/src/docs/2-Reference/2-Find my animation.mdx +0 -14
  24. package/src/docs/2-Reference/3-Multi brand support.mdx +0 -146
  25. package/src/docs/blocks/AnimationLibrary.tsx +0 -181
  26. package/src/docs/blocks/BrandSupportBadge.tsx +0 -190
  27. package/src/docs/blocks/BrandSupportFilter.tsx +0 -72
  28. package/src/docs/blocks/Cards.tsx +0 -74
  29. package/src/docs/blocks/IllustrationLibrary.tsx +0 -179
  30. package/src/docs/blocks/useStorybookTheme.ts +0 -73
  31. package/src/index.ts +0 -4
  32. package/src/scripts/agent-references/illustration-reference.test.ts +0 -100
  33. package/src/scripts/agent-references/illustration-reference.ts +0 -140
  34. package/src/scripts/generate-assets.ts +0 -649
  35. package/src/scripts/templates/assetTemplate.ts +0 -224
  36. package/src/scripts/templates/baseTypes.ts +0 -45
  37. package/src/scripts/templates/indexTemplate.ts +0 -109
  38. package/src/unity.css +0 -5
@@ -1,649 +0,0 @@
1
- import { existsSync, readFileSync } from 'fs'
2
- import fs from 'fs/promises'
3
- import path from 'path'
4
-
5
- import type { Config } from 'svgo'
6
-
7
- import type { ValidationEntry } from '../../scripts/shared/validate-size'
8
- import type { SizeGuardrailsConfig } from '../../scripts/steps/download-assets'
9
-
10
- import imageSize from 'image-size'
11
- import { loadConfig, optimize } from 'svgo'
12
-
13
- import {
14
- parseSvgWidth,
15
- reportViolations,
16
- } from '../../scripts/shared/validate-size'
17
- import {
18
- generateAssetModule,
19
- generateThemedSvgAssetModule,
20
- } from './templates/assetTemplate'
21
- import { generateBaseTypes } from './templates/baseTypes'
22
- import { generateIndexFile } from './templates/indexTemplate'
23
- import { writeIllustrationAgentReferences } from './agent-references/illustration-reference'
24
-
25
- // Configuration
26
- const illustrationsSvgDir = path.resolve('./assets/')
27
- const srcRoot = path.resolve('./src')
28
- const outputDir = path.join(srcRoot, 'generated')
29
- const assetsOutputDir = path.join(outputDir, 'assets') // Optimized assets go here
30
- const agentReferencesOutputDir = path.resolve('./src/agent-references')
31
- const configPath = path.resolve('./.figma-sync.json')
32
-
33
- const verboseFromArg = process.argv.some(
34
- arg => arg === '--verbose' || arg === '-v',
35
- )
36
- const verboseFromNx = ['1', 'true', 'yes', 'on'].includes(
37
- (process.env.NX_VERBOSE_LOGGING ?? '').toLowerCase(),
38
- )
39
- const isVerbose = verboseFromArg || verboseFromNx
40
- type IllustrationTheme = 'legacy' | 'rebrand'
41
-
42
- type AssetCategory = 'icon' | 'picture' | 'animation'
43
-
44
- type AssetType = {
45
- type: 'svg' | 'image'
46
- animated: boolean
47
- category?: AssetCategory
48
- theme?: IllustrationTheme
49
- }
50
-
51
- type ProcessedComponent = {
52
- name: string
53
- type: 'svg' | 'image'
54
- animated: boolean
55
- category?: AssetCategory
56
- theme?: IllustrationTheme
57
- dimensions?: { width: number; height: number }
58
- sizeBefore: number
59
- sizeAfter: number
60
- originalPath: string
61
- optimizedPath: string
62
- originalFileName: string
63
- }
64
-
65
- function verboseLog(message: string) {
66
- if (isVerbose) {
67
- console.log(message)
68
- }
69
- }
70
-
71
- // Utility functions
72
- function toPascalCase(str: string) {
73
- return str
74
- .replace(/([a-z])([A-Z])/g, '$1 $2')
75
- .replace(/[^a-z0-9]+/gi, ' ')
76
- .split(' ')
77
- .map(word => word.charAt(0).toUpperCase() + word.slice(1))
78
- .join('')
79
- }
80
-
81
- function getComponentName(filePath: string) {
82
- const filename = path.basename(filePath)
83
- const typeMatch = /Type=([^,.]+)/.exec(filename)
84
- const nameMatch = /Name=([^,.]+)/.exec(filename)
85
- const bgMatch = /With background=yes/.exec(filename)
86
- const type = typeMatch ? typeMatch[1] : null
87
- const name = nameMatch ? nameMatch[1] : 'Illustration'
88
- let base = ''
89
- if (type === 'empty-state') {
90
- base += 'EmptyState'
91
- }
92
- base += toPascalCase(name ?? '')
93
- if (bgMatch) {
94
- base += 'WithBackground'
95
- }
96
-
97
- // Add Animation suffix for animated illustrations
98
- const relativePath = path.relative(illustrationsSvgDir, filePath)
99
- const pathParts = relativePath.split(path.sep)
100
- if (pathParts.includes('animated-illustrations')) {
101
- base += 'Animation'
102
- }
103
-
104
- return base
105
- }
106
-
107
- async function getAllAssetFiles(dir: string) {
108
- let results: string[] = []
109
- const list = await fs.readdir(dir, { withFileTypes: true })
110
- for (const entry of list) {
111
- const filePath = path.join(dir, entry.name)
112
- if (entry.isDirectory()) {
113
- results = results.concat(await getAllAssetFiles(filePath))
114
- } else if (/\.(?:svg|gif|webp|png|jpg|jpeg)$/i.exec(filePath)) {
115
- results.push(filePath)
116
- }
117
- }
118
- return results
119
- }
120
-
121
- function getAssetTypeFromPath(filePath: string): AssetType | undefined {
122
- const relativePath = path.relative(illustrationsSvgDir, filePath)
123
- const pathParts = relativePath.split(path.sep)
124
- const [themeFolder, categoryFolder] = pathParts
125
- const ext = path.extname(filePath).toLowerCase()
126
-
127
- if (themeFolder === 'legacy' && categoryFolder === 'animated-illustrations') {
128
- if (ext !== '.webp') {
129
- throw new Error(
130
- `Animated illustrations must be WEBP format. Found: ${ext} in ${filePath}`,
131
- )
132
- }
133
- return {
134
- type: 'image' as const,
135
- animated: true,
136
- category: 'animation' as const,
137
- theme: 'legacy' as const,
138
- }
139
- }
140
-
141
- if (themeFolder === 'legacy' && categoryFolder === 'illustrated-icons') {
142
- if (ext !== '.svg') {
143
- throw new Error(
144
- `Legacy illustrated icons must be SVG format. Found: ${ext} in ${filePath}`,
145
- )
146
- }
147
- return {
148
- type: 'svg' as const,
149
- animated: false,
150
- category: 'icon' as const,
151
- theme: 'legacy' as const,
152
- }
153
- }
154
-
155
- if (themeFolder === 'legacy' && categoryFolder === 'illustrated-pictures') {
156
- if (ext !== '.svg') {
157
- throw new Error(
158
- `Legacy illustrated pictures must be SVG format. Found: ${ext} in ${filePath}`,
159
- )
160
- }
161
- return {
162
- type: 'svg' as const,
163
- animated: false,
164
- category: 'picture' as const,
165
- theme: 'legacy' as const,
166
- }
167
- }
168
-
169
- if (themeFolder === 'rebrand') {
170
- if (ext === '.svg') {
171
- return {
172
- type: 'svg' as const,
173
- animated: false,
174
- theme: 'rebrand' as const,
175
- }
176
- }
177
-
178
- if (
179
- ext === '.webp' &&
180
- path.basename(filePath).startsWith('Type=Animation')
181
- ) {
182
- return undefined
183
- }
184
-
185
- throw new Error(
186
- `Static rebrand illustration variants must be SVG format. Found: ${ext} in ${filePath}`,
187
- )
188
- }
189
-
190
- return undefined
191
- }
192
-
193
- function optimizeSvg(svgContent: string, filePath: string, svgoConfig: Config) {
194
- try {
195
- const result = optimize(svgContent, {
196
- ...svgoConfig,
197
- path: filePath,
198
- })
199
- return result.data
200
- } catch (error) {
201
- console.warn(
202
- `⚠️ SVG optimization failed for ${path.basename(
203
- filePath,
204
- )}, using original:`,
205
- (error as Error).message,
206
- )
207
- return svgContent
208
- }
209
- }
210
-
211
- async function getImageDimensions(
212
- imagePath: string,
213
- ): Promise<{ width: number; height: number }> {
214
- try {
215
- const buffer = await fs.readFile(imagePath)
216
- const dimensions = imageSize(buffer)
217
-
218
- if (!dimensions.width || !dimensions.height) {
219
- throw new Error(`Could not determine dimensions for ${imagePath}`)
220
- }
221
-
222
- return { width: dimensions.width, height: dimensions.height }
223
- } catch (error) {
224
- throw new Error(
225
- `Failed to get dimensions for ${imagePath}: ${(error as Error).message}`,
226
- )
227
- }
228
- }
229
-
230
- async function processAsset(assetPath: string, outputFileName?: string) {
231
- const assetFile = path.basename(assetPath)
232
- const componentName = getComponentName(assetPath)
233
- const assetType = getAssetTypeFromPath(assetPath)
234
- if (!assetType) {
235
- return undefined
236
- }
237
- const svgoConfig = (await loadConfig()) ?? {}
238
-
239
- // Generate output path in the generated/assets folder
240
- const ext = path.extname(assetPath)
241
- const optimizedOutputFileName =
242
- outputFileName ??
243
- (assetType.type === 'svg' && assetType.theme
244
- ? `${componentName}.${assetType.theme}${ext}`
245
- : `${componentName}${ext}`)
246
- const optimizedAssetPath = path.join(assetsOutputDir, optimizedOutputFileName)
247
-
248
- let sizeBefore = 0
249
- let sizeAfter = 0
250
- let dimensions: { width: number; height: number } | undefined
251
-
252
- if (assetType.type === 'svg') {
253
- // Optimize SVG and save to generated/assets folder
254
- const originalContent = await fs.readFile(assetPath, 'utf8')
255
- sizeBefore = originalContent.length
256
- const optimizedContent = optimizeSvg(originalContent, assetPath, svgoConfig)
257
- sizeAfter = optimizedContent.length
258
-
259
- // Write optimized content to generated/assets folder
260
- await fs.writeFile(optimizedAssetPath, optimizedContent, 'utf8')
261
-
262
- const reduction = (((sizeBefore - sizeAfter) / sizeBefore) * 100).toFixed(1)
263
- verboseLog(
264
- `📉 ${assetFile}: ${sizeBefore} → ${sizeAfter} bytes (-${reduction}%)`,
265
- )
266
- } else {
267
- // For images, copy to generated/assets folder (assume they're already optimized)
268
- await fs.copyFile(assetPath, optimizedAssetPath)
269
- const stats = await fs.stat(assetPath)
270
- sizeBefore = sizeAfter = stats.size
271
-
272
- // Extract dimensions for animated illustrations
273
- if (assetType.category === 'animation') {
274
- dimensions = await getImageDimensions(assetPath)
275
- verboseLog(
276
- `🎬 ${assetFile}: ${(sizeBefore / 1024).toFixed(2)}KB (animation ${
277
- dimensions.width
278
- }x${dimensions.height})`,
279
- )
280
- } else {
281
- verboseLog(
282
- `📦 ${assetFile}: ${(sizeBefore / 1024).toFixed(2)}KB (${
283
- assetType.animated ? 'animated ' : ''
284
- }${assetType.type})`,
285
- )
286
- }
287
- }
288
-
289
- return {
290
- name: componentName,
291
- type: assetType.type,
292
- animated: assetType.animated,
293
- category: assetType.category,
294
- theme: assetType.theme,
295
- dimensions,
296
- sizeBefore,
297
- sizeAfter,
298
- originalPath: assetPath,
299
- optimizedPath: optimizedAssetPath,
300
- originalFileName: assetFile,
301
- }
302
- }
303
-
304
- function groupStaticSvgAssets(components: ProcessedComponent[]) {
305
- const groups = new Map<
306
- string,
307
- Partial<Record<IllustrationTheme, ProcessedComponent>>
308
- >()
309
-
310
- for (const component of components) {
311
- if (component.type !== 'svg' || !component.theme) {
312
- continue
313
- }
314
-
315
- const group = groups.get(component.name) ?? {}
316
- if (group[component.theme]) {
317
- throw new Error(
318
- `Duplicate ${component.theme} asset for ${component.name}: ${component.originalFileName}`,
319
- )
320
- }
321
- group[component.theme] = component
322
- groups.set(component.name, group)
323
- }
324
-
325
- const groupedComponents: ProcessedComponent[] = []
326
- const missingRebrandFiles: string[] = []
327
-
328
- for (const [componentName, group] of groups) {
329
- if (group.legacy && !group.rebrand) {
330
- missingRebrandFiles.push(group.legacy.originalFileName)
331
- }
332
-
333
- if (group.legacy) {
334
- groupedComponents.push({
335
- ...group.legacy,
336
- name: componentName,
337
- sizeBefore: group.legacy.sizeBefore + (group.rebrand?.sizeBefore ?? 0),
338
- sizeAfter: group.legacy.sizeAfter + (group.rebrand?.sizeAfter ?? 0),
339
- })
340
- continue
341
- }
342
-
343
- if (group.rebrand) {
344
- groupedComponents.push({
345
- ...group.rebrand,
346
- name: componentName,
347
- })
348
- }
349
- }
350
-
351
- if (missingRebrandFiles.length > 0) {
352
- throw new Error(
353
- `Every legacy SVG asset must have a rebrand asset with the same generated name. Missing rebrand counterparts for: ${missingRebrandFiles.join(', ')}`,
354
- )
355
- }
356
-
357
- return groupedComponents
358
- }
359
-
360
- async function validateSvgGuardrails(
361
- component: ProcessedComponent,
362
- sizeGuardrails: SizeGuardrailsConfig,
363
- violations: ValidationEntry[],
364
- warnings: ValidationEntry[],
365
- ) {
366
- const optimizedContent = await fs.readFile(component.optimizedPath, 'utf8')
367
- const optimizedSize = Buffer.byteLength(optimizedContent, 'utf8')
368
- const width = parseSvgWidth(optimizedContent)
369
-
370
- const exceedsFileSize = optimizedSize > sizeGuardrails.maxFileSize
371
- const exceedsWidth = width !== undefined && width > sizeGuardrails.maxWidth
372
- const isAllowlisted = sizeGuardrails.allowlist.includes(
373
- component.originalFileName,
374
- )
375
-
376
- if (!exceedsFileSize && !exceedsWidth) {
377
- return
378
- }
379
-
380
- const details: string[] = []
381
- if (exceedsFileSize) {
382
- details.push(
383
- `${(optimizedSize / 1024).toFixed(1)}KB after SVGO (limit: ${(
384
- sizeGuardrails.maxFileSize / 1024
385
- ).toFixed(0)}KB)`,
386
- )
387
- }
388
- if (exceedsWidth) {
389
- details.push(`${width}px width (limit: ${sizeGuardrails.maxWidth}px)`)
390
- }
391
-
392
- const detailStr = details.join('; ')
393
- const sourceLabel = component.theme
394
- ? `[${component.theme}] ${component.originalFileName}`
395
- : component.originalFileName
396
-
397
- if (isAllowlisted) {
398
- const msg = `${sourceLabel}: ${detailStr}. Consider simplifying this illustration in Figma.`
399
- warnings.push({
400
- filename: sourceLabel,
401
- message: msg,
402
- })
403
- console.warn(` ⚠️ [Allowlisted] ${msg}`)
404
- return
405
- }
406
-
407
- const msg = `${sourceLabel}: ${detailStr}. Automatic optimization could not reduce below limit — simplify in Figma.`
408
- violations.push({
409
- filename: sourceLabel,
410
- message: msg,
411
- })
412
- console.error(` ❌ ${msg}`)
413
- }
414
-
415
- function loadSizeGuardrails(): SizeGuardrailsConfig | undefined {
416
- if (!existsSync(configPath)) {
417
- console.log('ℹ️ No .figma-sync.json found, skipping size guardrails')
418
- return undefined
419
- }
420
-
421
- const config = JSON.parse(readFileSync(configPath, 'utf-8')) as {
422
- sizeGuardrails?: SizeGuardrailsConfig
423
- }
424
-
425
- if (!config.sizeGuardrails) {
426
- console.log('ℹ️ No sizeGuardrails config, skipping size validation')
427
- return undefined
428
- }
429
-
430
- return config.sizeGuardrails
431
- }
432
-
433
- async function main() {
434
- console.log('🎨 Generating and optimizing illustration assets...')
435
- if (isVerbose) {
436
- console.log('🔎 Verbose mode enabled (full asset logs)')
437
- }
438
-
439
- // Clear generated output so old asset names do not survive generator changes.
440
- await fs.rm(outputDir, { recursive: true, force: true })
441
-
442
- // Ensure output directories exist
443
- await fs.mkdir(outputDir, { recursive: true })
444
- await fs.mkdir(assetsOutputDir, { recursive: true })
445
-
446
- // Generate base types file
447
- const baseTypesContent = generateBaseTypes()
448
- await fs.writeFile(path.join(outputDir, 'types.ts'), baseTypesContent, 'utf8')
449
- verboseLog('✅ Generated base types')
450
-
451
- // Load size guardrails config
452
- const sizeGuardrails = loadSizeGuardrails()
453
- const violations: ValidationEntry[] = []
454
- const warnings: ValidationEntry[] = []
455
-
456
- // Discover all asset files
457
- const assetFiles = await getAllAssetFiles(illustrationsSvgDir)
458
- console.log(`🔍 Found ${assetFiles.length} assets to process`)
459
-
460
- if (assetFiles.length === 0) {
461
- console.warn('⚠️ No assets found! Check your assets directory.')
462
- return
463
- }
464
-
465
- // Process all assets
466
- const processedComponents: ProcessedComponent[] = []
467
- let hasProcessingErrors = false
468
- for (const assetPath of assetFiles) {
469
- try {
470
- const component = await processAsset(assetPath)
471
- if (!component) {
472
- verboseLog(`↪️ Skipped unsupported asset: ${path.basename(assetPath)}`)
473
- continue
474
- }
475
-
476
- // Validate optimized SVG against size guardrails
477
- if (sizeGuardrails && component.type === 'svg') {
478
- await validateSvgGuardrails(
479
- component,
480
- sizeGuardrails,
481
- violations,
482
- warnings,
483
- )
484
- }
485
-
486
- processedComponents.push(component)
487
- verboseLog(
488
- `✅ ${component.originalFileName} → optimized ${path.basename(
489
- component.optimizedPath,
490
- )}`,
491
- )
492
-
493
- if (!isVerbose && processedComponents.length % 25 === 0) {
494
- console.log(
495
- `⏳ Processed ${processedComponents.length}/${assetFiles.length} assets...`,
496
- )
497
- }
498
- } catch (error) {
499
- hasProcessingErrors = true
500
- console.error(
501
- `❌ Failed to process ${assetPath}:`,
502
- (error as Error).message,
503
- )
504
- if (isVerbose) {
505
- console.error(error)
506
- }
507
- }
508
- }
509
-
510
- if (hasProcessingErrors) {
511
- throw new Error('One or more illustration assets failed to process')
512
- }
513
-
514
- const staticComponents = groupStaticSvgAssets(processedComponents)
515
- const animatedComponents = processedComponents.filter(
516
- component => component.category === 'animation',
517
- )
518
- const components = [...staticComponents, ...animatedComponents].sort((a, b) =>
519
- a.name.localeCompare(b.name),
520
- )
521
-
522
- const staticGroups = new Map<
523
- string,
524
- Partial<Record<IllustrationTheme, ProcessedComponent>>
525
- >()
526
- for (const component of processedComponents) {
527
- if (component.type === 'svg' && component.theme) {
528
- const group = staticGroups.get(component.name) ?? {}
529
- group[component.theme] = component
530
- staticGroups.set(component.name, group)
531
- }
532
- }
533
-
534
- for (const component of components) {
535
- try {
536
- let moduleContent: string
537
- if (component.type === 'svg') {
538
- const group = staticGroups.get(component.name)
539
- if (!group?.legacy && !group?.rebrand) {
540
- throw new Error(`Missing SVG source for ${component.name}`)
541
- }
542
- const defaultComponent = group.legacy ?? group.rebrand
543
- if (!defaultComponent) {
544
- throw new Error(`Missing default SVG source for ${component.name}`)
545
- }
546
- moduleContent = generateThemedSvgAssetModule(
547
- component.name,
548
- {
549
- default: defaultComponent.optimizedPath,
550
- legacy: group.legacy?.optimizedPath,
551
- rebrand: group.rebrand?.optimizedPath,
552
- },
553
- {
554
- default: defaultComponent.originalFileName,
555
- legacy: group.legacy?.originalFileName,
556
- rebrand: group.rebrand?.originalFileName,
557
- },
558
- )
559
- } else {
560
- moduleContent = generateAssetModule(
561
- component.name,
562
- component.optimizedPath,
563
- {
564
- type: component.type,
565
- animated: component.animated,
566
- category: component.category,
567
- dimensions: component.dimensions,
568
- },
569
- component.originalFileName,
570
- )
571
- }
572
-
573
- const outPath = path.join(outputDir, `${component.name}.ts`)
574
- await fs.writeFile(outPath, moduleContent, 'utf8')
575
-
576
- verboseLog(
577
- `✅ ${component.originalFileName} → ${component.name}.ts + optimized asset`,
578
- )
579
- } catch (error) {
580
- console.error(
581
- `❌ Failed to generate ${component.name}:`,
582
- (error as Error).message,
583
- )
584
- if (isVerbose) {
585
- console.error(error)
586
- }
587
- }
588
- }
589
-
590
- // Generate main index file
591
- const indexContent = generateIndexFile(components)
592
- await fs.writeFile(
593
- path.join(outputDir, 'illustrationAssets.ts'),
594
- indexContent,
595
- 'utf8',
596
- )
597
- writeIllustrationAgentReferences({
598
- components,
599
- outputDir: agentReferencesOutputDir,
600
- })
601
-
602
- // Summary
603
- const totalSizeBefore = components.reduce((sum, c) => sum + c.sizeBefore, 0)
604
- const totalSizeAfter = components.reduce((sum, c) => sum + c.sizeAfter, 0)
605
- const totalReduction =
606
- totalSizeBefore > 0
607
- ? (((totalSizeBefore - totalSizeAfter) / totalSizeBefore) * 100).toFixed(
608
- 1,
609
- )
610
- : '0'
611
-
612
- console.log(`\n🎉 Generated ${components.length} illustration assets!`)
613
- console.log(
614
- `📊 Total optimization: ${(totalSizeBefore / 1024).toFixed(1)}KB → ${(
615
- totalSizeAfter / 1024
616
- ).toFixed(1)}KB (-${totalReduction}%)`,
617
- )
618
- console.log(
619
- `📁 Optimized assets saved to: ${path.relative(
620
- process.cwd(),
621
- assetsOutputDir,
622
- )}`,
623
- )
624
- console.log(` - SVGs: ${components.filter(c => c.type === 'svg').length}`)
625
- console.log(
626
- ` - Images: ${
627
- components.filter(c => c.type === 'image' && c.category !== 'animation')
628
- .length
629
- }`,
630
- )
631
- console.log(
632
- ` - Animations: ${
633
- components.filter(c => c.category === 'animation').length
634
- }`,
635
- )
636
- console.log(
637
- ` - Total Animated: ${components.filter(c => c.animated).length}`,
638
- )
639
-
640
- // Report size guardrail results
641
- if (sizeGuardrails) {
642
- reportViolations(violations, warnings, sizeGuardrails.strict)
643
- }
644
- }
645
-
646
- main().catch((err: unknown) => {
647
- console.error('❌ Error generating illustrations:', err)
648
- process.exit(1)
649
- })