@liner-fe/internal-icon 1.0.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/.ultra.cache.json +1 -0
- package/CHANGELOG.md +19 -0
- package/README.md +3 -0
- package/figma.json +1279 -0
- package/package.json +13 -0
- package/scripts/generate-svg-files.js +36 -0
- package/scripts/generate-tsx-components.js +179 -0
package/package.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@liner-fe/internal-icon",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"scripts": {
|
|
8
|
+
"generate-svg": "node scripts/generate-svg-files.js",
|
|
9
|
+
"generate-tsx": "node scripts/generate-tsx-components.js",
|
|
10
|
+
"build": "pnpm generate-svg && pnpm generate-tsx",
|
|
11
|
+
"build:package": "pnpm build"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const figmaData = JSON.parse(fs.readFileSync('./figma.json', 'utf8'));
|
|
5
|
+
|
|
6
|
+
function convertToCurrentColor(svgString) {
|
|
7
|
+
return svgString.replace(/#1E1E1F/g, 'currentColor');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function optimizeSvg(svgString) {
|
|
11
|
+
return svgString.replace(/\s+/g, ' ').replace(/>\s+</g, '><').trim();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const svgDir = './';
|
|
15
|
+
if (!fs.existsSync(svgDir)) {
|
|
16
|
+
fs.mkdirSync(svgDir, { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
Object.entries(figmaData).forEach(([iconName, variants]) => {
|
|
20
|
+
const iconDir = path.join(svgDir, iconName);
|
|
21
|
+
if (!fs.existsSync(iconDir)) {
|
|
22
|
+
fs.mkdirSync(iconDir, { recursive: true });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
Object.entries(variants).forEach(([variant, svgString]) => {
|
|
26
|
+
if (svgString && svgString.trim() !== '') {
|
|
27
|
+
const convertedSvg = convertToCurrentColor(svgString);
|
|
28
|
+
const optimizedSvg = optimizeSvg(convertedSvg);
|
|
29
|
+
|
|
30
|
+
const fileName = `${variant}.svg`;
|
|
31
|
+
const filePath = path.join(iconDir, fileName);
|
|
32
|
+
|
|
33
|
+
fs.writeFileSync(filePath, optimizedSvg);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
// 상수 정의
|
|
5
|
+
const FIGMA_DATA_PATH = './figma.json';
|
|
6
|
+
const OUTPUT_DIR = './assets';
|
|
7
|
+
const INDEX_FILE_PATH = './index.tsx';
|
|
8
|
+
|
|
9
|
+
const COLOR = 'type';
|
|
10
|
+
const FILL_TYPE = 'fillType';
|
|
11
|
+
|
|
12
|
+
// SVG 색상 변환 함수
|
|
13
|
+
function convertToCurrentColor(svgString) {
|
|
14
|
+
return svgString
|
|
15
|
+
.replace(/fill="#1E1E1F"/g, `style={{ fill: \`var(--$\{${COLOR}\})\` }}`)
|
|
16
|
+
.replace(/fill="white"/g, `style={{ fill: \`var(--$\{${FILL_TYPE}\})\` }}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// SVG 속성 정규화 함수
|
|
20
|
+
function normalizeSvgAttributes(svgString) {
|
|
21
|
+
return svgString
|
|
22
|
+
.replace(/fill-rule/g, 'fillRule')
|
|
23
|
+
.replace(/clip-rule/g, 'clipRule')
|
|
24
|
+
.replace(/width="\d+"/, 'width={iconSizeMap[size]}')
|
|
25
|
+
.replace(/height="\d+"/, 'height={iconSizeMap[size]}')
|
|
26
|
+
.replace(/xmlns="http:\/\/www.w3.org\/2000\/svg"/, 'ref={ref} className={className} {...props}')
|
|
27
|
+
.replace(/style="[^"]*"/g, '')
|
|
28
|
+
.replace(/xmlns="http:\/\/www.w3.org\/1999\/xhtml"/g, '');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 케밥 케이스 변환 함수
|
|
32
|
+
const KEBAB_REGEX = /\p{Lu}/gu;
|
|
33
|
+
const kebabCase = (str, keepLeadingDash = true) => {
|
|
34
|
+
const result = str.replace(KEBAB_REGEX, (match) => `-${match.toLowerCase()}`);
|
|
35
|
+
|
|
36
|
+
if (keepLeadingDash) {
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (result.startsWith('-')) {
|
|
41
|
+
return result.slice(1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return result;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// 파스칼 케이스 변환 함수
|
|
48
|
+
const generateIconName = (iconName) => {
|
|
49
|
+
return iconName
|
|
50
|
+
.split('-')
|
|
51
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
52
|
+
.join('');
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// 아이콘 Props 인터페이스 생성 함수
|
|
56
|
+
const IconProps = (additionalProps = '') => `{
|
|
57
|
+
fill?: boolean;
|
|
58
|
+
thick?: boolean;
|
|
59
|
+
className?: string;
|
|
60
|
+
${additionalProps}
|
|
61
|
+
${COLOR}?: BasicColorType;
|
|
62
|
+
${FILL_TYPE}?: BasicColorType;
|
|
63
|
+
}`;
|
|
64
|
+
|
|
65
|
+
// 디렉토리 생성 함수
|
|
66
|
+
function ensureDirectoryExists(dirPath) {
|
|
67
|
+
if (!fs.existsSync(dirPath)) {
|
|
68
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// SVG 변형 처리 함수
|
|
73
|
+
function processSvgVariants(variants) {
|
|
74
|
+
const variantSvgs = {};
|
|
75
|
+
|
|
76
|
+
Object.entries(variants).forEach(([variant, svgString]) => {
|
|
77
|
+
if (svgString && svgString.trim() !== '') {
|
|
78
|
+
variantSvgs[variant] = normalizeSvgAttributes(convertToCurrentColor(svgString));
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return variantSvgs;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// TSX 컴포넌트 코드 생성 함수
|
|
86
|
+
function generateTsxComponent(componentName, variantSvgs, defaultSvg) {
|
|
87
|
+
return `import React, { forwardRef } from 'react';
|
|
88
|
+
import { iconSizeMap } from '@liner-fe/design-token-primitive';
|
|
89
|
+
import { IconProps } from '../../index';
|
|
90
|
+
|
|
91
|
+
export const Icon${componentName} = forwardRef<SVGSVGElement, IconProps>(
|
|
92
|
+
({ fill = false, thick = false, size = 'm', type = 'neutral-label-primary', className, fillType = 'inverse-label-primary', ...props }, ref) => {
|
|
93
|
+
if (fill && thick) {
|
|
94
|
+
return ${variantSvgs['fill-thick'] || defaultSvg};
|
|
95
|
+
} else if (fill) {
|
|
96
|
+
return ${variantSvgs.fill || defaultSvg};
|
|
97
|
+
} else if (thick) {
|
|
98
|
+
return ${variantSvgs.thick || defaultSvg};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return ${variantSvgs.default || defaultSvg};
|
|
102
|
+
}
|
|
103
|
+
);
|
|
104
|
+
`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 메인 실행 함수
|
|
108
|
+
function generateIconComponents() {
|
|
109
|
+
const figmaData = JSON.parse(fs.readFileSync(FIGMA_DATA_PATH, 'utf8'));
|
|
110
|
+
|
|
111
|
+
// 출력 디렉토리 생성
|
|
112
|
+
ensureDirectoryExists(OUTPUT_DIR);
|
|
113
|
+
|
|
114
|
+
Object.entries(figmaData).forEach(([iconName, variants]) => {
|
|
115
|
+
const iconDir = path.join(OUTPUT_DIR, iconName);
|
|
116
|
+
ensureDirectoryExists(iconDir);
|
|
117
|
+
|
|
118
|
+
const variantSvgs = processSvgVariants(variants);
|
|
119
|
+
|
|
120
|
+
if (Object.keys(variantSvgs).length > 0) {
|
|
121
|
+
const componentName = generateIconName(iconName);
|
|
122
|
+
const defaultSvg = variantSvgs.default || variantSvgs.thick || Object.values(variantSvgs)[0];
|
|
123
|
+
|
|
124
|
+
const tsxCode = generateTsxComponent(componentName, variantSvgs, defaultSvg);
|
|
125
|
+
const filePath = path.join(iconDir, 'index.tsx');
|
|
126
|
+
|
|
127
|
+
fs.writeFileSync(filePath, tsxCode);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 인덱스 파일 생성 함수
|
|
133
|
+
function generateIndexFile() {
|
|
134
|
+
const figmaData = JSON.parse(fs.readFileSync(FIGMA_DATA_PATH, 'utf8'));
|
|
135
|
+
|
|
136
|
+
const imports = Object.keys(figmaData)
|
|
137
|
+
.map((iconName) => {
|
|
138
|
+
const componentName = generateIconName(iconName);
|
|
139
|
+
return `export { Icon${componentName} } from './assets/${iconName}';`;
|
|
140
|
+
})
|
|
141
|
+
.join('\n');
|
|
142
|
+
|
|
143
|
+
const iconKeys = Object.keys(figmaData).map((iconName) => {
|
|
144
|
+
return `"${kebabCase(generateIconName(iconName), false)}"`;
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const indexContent = `import React, { SVGProps } from 'react';
|
|
148
|
+
import { BasicColorType, iconSizeMap } from '@liner-fe/design-token-primitive';
|
|
149
|
+
${imports}
|
|
150
|
+
|
|
151
|
+
export type IconName = ${iconKeys.join(' | ')};
|
|
152
|
+
|
|
153
|
+
export type IconSizeKey = keyof typeof iconSizeMap;
|
|
154
|
+
|
|
155
|
+
export interface IconProps extends Omit<SVGProps<SVGSVGElement>, 'fill' | 'name'> ${IconProps('size?: keyof typeof iconSizeMap;')}
|
|
156
|
+
|
|
157
|
+
export type IconComponentType = React.ForwardRefExoticComponent<
|
|
158
|
+
Omit<IconProps, 'ref'> & React.RefAttributes<SVGSVGElement>
|
|
159
|
+
>;
|
|
160
|
+
`;
|
|
161
|
+
|
|
162
|
+
fs.writeFileSync(INDEX_FILE_PATH, indexContent, (err) => {
|
|
163
|
+
if (err) {
|
|
164
|
+
console.error(err);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
console.log('Index file generated successfully');
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 메인 실행
|
|
172
|
+
function main() {
|
|
173
|
+
generateIconComponents();
|
|
174
|
+
generateIndexFile();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
console.log('실행');
|
|
178
|
+
|
|
179
|
+
main();
|