@payfit/unity-illustrations 2.55.22 → 2.56.0
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.
- package/package.json +11 -4
- package/skills/unity-illustrations/SKILL.md +39 -0
- package/src/agent-references/illustrations-catalog.json +1996 -0
- package/src/agent-references/manifest.json +16 -0
- package/dist/esm/scripts/agent-references/illustration-reference.d.ts +0 -17
- package/src/agent-references/README.mdx +0 -17
- package/src/agent-references/illustrations.mdx +0 -74
- package/src/components/illustration/Illustration.stories.tsx +0 -362
- package/src/components/illustration/Illustration.tsx +0 -169
- package/src/components/illustration/figma/IllustrationLarge.figma.ts +0 -258
- package/src/components/illustration/figma/IllustrationMedium.figma.ts +0 -258
- package/src/components/illustration/figma/IllustrationSmall.figma.ts +0 -258
- package/src/components/lazy-illustration/LazyIllustration.stories.tsx +0 -204
- package/src/components/lazy-illustration/LazyIllustration.tsx +0 -125
- package/src/components/lazy-illustration/figma/LazyIllustrationLarge.figma.ts +0 -255
- package/src/components/lazy-illustration/figma/LazyIllustrationMedium.figma.ts +0 -255
- package/src/components/lazy-illustration/figma/LazyIllustrationSmall.figma.ts +0 -255
- package/src/docs/1-Introduction/1-Welcome.mdx +0 -111
- package/src/docs/1-Introduction/2-Getting Started.mdx +0 -25
- package/src/docs/1-Introduction/3-Usage.mdx +0 -114
- package/src/docs/1-Introduction/4-Changelog.mdx +0 -6
- package/src/docs/2-Reference/1-Find my illustration.mdx +0 -12
- package/src/docs/2-Reference/2-Find my animation.mdx +0 -14
- package/src/docs/2-Reference/3-Multi brand support.mdx +0 -146
- package/src/docs/blocks/AnimationLibrary.tsx +0 -181
- package/src/docs/blocks/BrandSupportBadge.tsx +0 -190
- package/src/docs/blocks/BrandSupportFilter.tsx +0 -72
- package/src/docs/blocks/Cards.tsx +0 -74
- package/src/docs/blocks/IllustrationLibrary.tsx +0 -179
- package/src/docs/blocks/useStorybookTheme.ts +0 -73
- package/src/index.ts +0 -4
- package/src/scripts/agent-references/illustration-reference.test.ts +0 -100
- package/src/scripts/agent-references/illustration-reference.ts +0 -140
- package/src/scripts/generate-assets.ts +0 -649
- package/src/scripts/templates/assetTemplate.ts +0 -224
- package/src/scripts/templates/baseTypes.ts +0 -45
- package/src/scripts/templates/indexTemplate.ts +0 -109
- 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
|
-
})
|