@iyulab/components 1.9.0 → 1.12.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.
Files changed (120) hide show
  1. package/CHANGELOG.md +107 -9
  2. package/README.md +11 -1
  3. package/dist/_virtual/{_@oxc-project_runtime@0.139.0 → _@oxc-project_runtime@0.142.0}/helpers/esm/decorate.js +1 -1
  4. package/dist/_virtual/{_@oxc-project_runtime@0.139.0 → _@oxc-project_runtime@0.142.0}/helpers/esm/decorateMetadata.js +1 -1
  5. package/dist/_virtual/_glob-assets_raw/dark.css.e8ac080d.js +1 -1
  6. package/dist/_virtual/_glob-assets_raw/light.css.637cb2f8.js +1 -1
  7. package/dist/components/UElement.d.ts +1 -4
  8. package/dist/components/UElement.js +3 -4
  9. package/dist/components/UElement.styles.js +2 -2
  10. package/dist/components/UFloatingElement.js +2 -2
  11. package/dist/components/UFormControlElement.js +2 -2
  12. package/dist/components/UOverlayElement.d.ts +3 -0
  13. package/dist/components/UOverlayElement.js +5 -2
  14. package/dist/components/alert/UAlert.d.ts +3 -0
  15. package/dist/components/alert/UAlert.js +2 -2
  16. package/dist/components/alert/UAlert.styles.js +29 -19
  17. package/dist/components/avatar/UAvatar.js +2 -2
  18. package/dist/components/avatar/UAvatar.styles.js +2 -2
  19. package/dist/components/badge/UBadge.d.ts +2 -0
  20. package/dist/components/badge/UBadge.js +7 -5
  21. package/dist/components/badge/UBadge.styles.js +16 -5
  22. package/dist/components/breadcrumb/UBreadcrumb.js +1 -1
  23. package/dist/components/breadcrumb-item/UBreadcrumbItem.js +2 -2
  24. package/dist/components/breadcrumb-item/UBreadcrumbItem.styles.js +1 -1
  25. package/dist/components/button/UButton.d.ts +19 -2
  26. package/dist/components/button/UButton.js +2 -2
  27. package/dist/components/button/UButton.styles.js +34 -23
  28. package/dist/components/button-group/UButtonGroup.js +2 -2
  29. package/dist/components/card/UCard.d.ts +2 -0
  30. package/dist/components/card/UCard.js +4 -2
  31. package/dist/components/card/UCard.styles.js +17 -3
  32. package/dist/components/carousel/UCarousel.js +2 -2
  33. package/dist/components/carousel/UCarousel.styles.js +1 -1
  34. package/dist/components/checkbox/UCheckbox.d.ts +2 -0
  35. package/dist/components/checkbox/UCheckbox.js +2 -2
  36. package/dist/components/checkbox/UCheckbox.styles.js +39 -99
  37. package/dist/components/chip/UChip.js +2 -2
  38. package/dist/components/copy-button/UCopyButton.js +2 -2
  39. package/dist/components/copy-button/UCopyButton.styles.js +2 -2
  40. package/dist/components/dialog/UDialog.js +2 -2
  41. package/dist/components/dialog/UDialog.styles.js +2 -2
  42. package/dist/components/divider/UDivider.js +2 -2
  43. package/dist/components/drawer/UDrawer.d.ts +1 -1
  44. package/dist/components/drawer/UDrawer.js +2 -2
  45. package/dist/components/drawer/UDrawer.styles.js +1 -1
  46. package/dist/components/field/UField.js +2 -2
  47. package/dist/components/field/UField.styles.js +2 -2
  48. package/dist/components/form/UForm.js +2 -2
  49. package/dist/components/icon/UIcon.js +2 -2
  50. package/dist/components/icon-button/UIconButton.js +2 -2
  51. package/dist/components/icon-button/UIconButton.styles.js +1 -1
  52. package/dist/components/input/UInput.d.ts +4 -0
  53. package/dist/components/input/UInput.js +2 -4
  54. package/dist/components/input/UInput.styles.js +6 -3
  55. package/dist/components/menu/UMenu.d.ts +3 -0
  56. package/dist/components/menu/UMenu.js +5 -4
  57. package/dist/components/menu/UMenu.styles.js +19 -6
  58. package/dist/components/menu-item/UMenuItem.d.ts +2 -0
  59. package/dist/components/menu-item/UMenuItem.js +3 -2
  60. package/dist/components/menu-item/UMenuItem.styles.js +2 -2
  61. package/dist/components/option/UOption.d.ts +4 -0
  62. package/dist/components/option/UOption.js +9 -7
  63. package/dist/components/option/UOption.styles.js +17 -6
  64. package/dist/components/panel/UPanel.js +2 -2
  65. package/dist/components/popover/UPopover.js +2 -3
  66. package/dist/components/progress-bar/UProgressBar.js +4 -3
  67. package/dist/components/progress-bar/UProgressBar.styles.js +10 -10
  68. package/dist/components/progress-ring/UProgressRing.js +2 -2
  69. package/dist/components/progress-ring/UProgressRing.styles.js +9 -9
  70. package/dist/components/radio/URadio.d.ts +3 -0
  71. package/dist/components/radio/URadio.js +2 -3
  72. package/dist/components/radio/URadio.styles.js +2 -2
  73. package/dist/components/rating/URating.js +2 -2
  74. package/dist/components/rating/URating.styles.js +2 -2
  75. package/dist/components/select/USelect.js +2 -4
  76. package/dist/components/select/USelect.styles.js +3 -3
  77. package/dist/components/skeleton/USkeleton.js +2 -2
  78. package/dist/components/skeleton/USkeleton.styles.js +3 -3
  79. package/dist/components/slider/USlider.js +2 -2
  80. package/dist/components/slider/USlider.styles.js +4 -4
  81. package/dist/components/spinner/USpinner.js +2 -2
  82. package/dist/components/spinner/USpinner.styles.js +1 -1
  83. package/dist/components/split-panel/USplitPanel.js +2 -2
  84. package/dist/components/split-panel/USplitPanel.styles.js +2 -2
  85. package/dist/components/switch/USwitch.js +2 -2
  86. package/dist/components/switch/USwitch.styles.js +4 -4
  87. package/dist/components/tab/UTab.d.ts +2 -0
  88. package/dist/components/tab/UTab.js +9 -7
  89. package/dist/components/tab/UTab.styles.js +15 -3
  90. package/dist/components/tab-panel/UTabPanel.d.ts +2 -0
  91. package/dist/components/tab-panel/UTabPanel.js +2 -2
  92. package/dist/components/tab-panel/UTabPanel.styles.js +3 -3
  93. package/dist/components/tag/UTag.d.ts +4 -0
  94. package/dist/components/tag/UTag.js +9 -7
  95. package/dist/components/tag/UTag.styles.js +101 -170
  96. package/dist/components/textarea/UTextarea.js +2 -2
  97. package/dist/components/textarea/UTextarea.styles.js +2 -2
  98. package/dist/components/tooltip/UTooltip.d.ts +2 -0
  99. package/dist/components/tooltip/UTooltip.js +5 -3
  100. package/dist/components/tooltip/UTooltip.styles.js +12 -3
  101. package/dist/components/tree/UTree.d.ts +6 -0
  102. package/dist/components/tree/UTree.js +2 -2
  103. package/dist/components/tree-item/UTreeItem.d.ts +2 -0
  104. package/dist/components/tree-item/UTreeItem.js +2 -2
  105. package/dist/components/tree-item/UTreeItem.styles.js +3 -3
  106. package/dist/react/UDialog.d.ts +5 -1
  107. package/dist/react/UDialog.js +4 -1
  108. package/dist/react/UDrawer.d.ts +5 -1
  109. package/dist/react/UDrawer.js +4 -1
  110. package/dist/react/UPopover.d.ts +4 -2
  111. package/dist/react/UTooltip.d.ts +4 -2
  112. package/dist/styles/dark.css +64 -7
  113. package/dist/styles/light.css +64 -7
  114. package/dist/styles/tokens.css +17 -0
  115. package/dist/utilities/Theme.js +4 -0
  116. package/package.json +7 -4
  117. package/plugins/vite-plugin-glob-resolve.d.ts +0 -12
  118. package/plugins/vite-plugin-glob-resolve.js +0 -66
  119. package/plugins/vite-plugin-react-wrapper.d.ts +0 -14
  120. package/plugins/vite-plugin-react-wrapper.js +0 -235
@@ -132,34 +132,91 @@
132
132
  --u-pink-900: #880E4F;
133
133
  --u-pink-1000: #520830;
134
134
 
135
+ /* ==========================================================================
136
+ Role Colors — 의미 역할 층
137
+ 컴포넌트는 팔레트 프리미티브가 아니라 이 층을 참조한다.
138
+ 브랜드 색 교체는 아래 25개만 재정의하면 시맨틱 층까지 따라온다.
139
+
140
+ 단은 '강도' 축이다 (weakest -> strong). 용도(배경/테두리/텍스트)는
141
+ 소비처가 정한다 -- 단을 속성에 묶으면 primary 버튼의 배경이 어느
142
+ 단인지 같은 모순이 생긴다.
143
+ ========================================================================== */
144
+
145
+ /* primary — 브랜드 · 강조 · 포커스 · 링크 */
146
+ --u-primary-color-weakest: var(--u-blue-200);
147
+ --u-primary-color-weaker: var(--u-blue-300);
148
+ --u-primary-color-weak: var(--u-blue-500);
149
+ --u-primary-color: var(--u-blue-600);
150
+ --u-primary-color-strong: var(--u-blue-700);
151
+
152
+ /* info — 정보 안내 (primary 와 기본값은 같으나 역할이 다르다 — 브랜딩은 primary 만 바꾼다) */
153
+ --u-info-color-weakest: var(--u-blue-200);
154
+ --u-info-color-weaker: var(--u-blue-300);
155
+ --u-info-color-weak: var(--u-blue-500);
156
+ --u-info-color: var(--u-blue-600);
157
+ --u-info-color-strong: var(--u-blue-700);
158
+
159
+ /* success — 성공 · 완료 */
160
+ --u-success-color-weakest: var(--u-green-200);
161
+ --u-success-color-weaker: var(--u-green-300);
162
+ --u-success-color-weak: var(--u-green-500);
163
+ --u-success-color: var(--u-green-600);
164
+ --u-success-color-strong: var(--u-green-700);
165
+
166
+ /* warning — 주의 */
167
+ --u-warning-color-weakest: var(--u-yellow-200);
168
+ --u-warning-color-weaker: var(--u-yellow-300);
169
+ --u-warning-color-weak: var(--u-yellow-500);
170
+ --u-warning-color: var(--u-yellow-600);
171
+ --u-warning-color-strong: var(--u-yellow-700);
172
+
173
+ /* danger — 오류 · 위험 · 유효성 실패 */
174
+ --u-danger-color-weakest: var(--u-red-200);
175
+ --u-danger-color-weaker: var(--u-red-300);
176
+ --u-danger-color-weak: var(--u-red-500);
177
+ --u-danger-color: var(--u-red-600);
178
+ --u-danger-color-strong: var(--u-red-700);
179
+ /* ==========================================================================
180
+ Radius Scale — 모서리 반경
181
+ 리터럴을 흩뿌리면 값이 갈린다(실제로 pill 이 999px/9999px 두 표기로 나뉘어 있었다).
182
+ ========================================================================== */
183
+ --u-radius-none: 0;
184
+ --u-radius-sm: 3px;
185
+ --u-radius-md: 4px;
186
+ --u-radius-lg: 6px;
187
+ --u-radius-xl: 8px;
188
+ --u-radius-pill: 9999px;
189
+ --u-radius-circle: 50%;
190
+
135
191
  /* Text Colors */
136
192
  --u-txt-color: var(--u-neutral-900);
137
193
  --u-txt-color-inverse: var(--u-neutral-0);
138
- --u-txt-color-hover: var(--u-blue-600);
139
- --u-txt-color-active: var(--u-blue-600);
194
+ --u-txt-color-hover: var(--u-primary-color);
195
+ --u-txt-color-active: var(--u-primary-color);
140
196
  --u-txt-color-disabled: var(--u-neutral-400);
141
197
  --u-txt-color-weak: var(--u-neutral-500);
142
198
  --u-txt-color-strong: var(--u-neutral-1000);
143
199
 
144
- --u-link-txt-color: var(--u-blue-700);
200
+ --u-link-txt-color: var(--u-primary-color-strong);
145
201
  --u-tooltip-txt-color: var(--u-neutral-0);
146
202
 
147
203
  /* Icon Colors */
148
204
  --u-icon-color: var(--u-neutral-700);
149
205
  --u-icon-color-inverse: var(--u-neutral-0);
150
- --u-icon-color-hover: var(--u-blue-600);
151
- --u-icon-color-active: var(--u-blue-600);
206
+ --u-icon-color-hover: var(--u-primary-color);
207
+ --u-icon-color-active: var(--u-primary-color);
152
208
  --u-icon-color-disabled: var(--u-neutral-400);
153
209
 
154
210
  /* Border Colors */
155
211
  --u-border-color: var(--u-neutral-300);
156
212
  --u-border-color-weak: var(--u-neutral-200);
157
213
  --u-border-color-strong: var(--u-neutral-400);
214
+ --u-border-color-hover: var(--u-neutral-400);
158
215
 
159
216
  --u-input-border-color: var(--u-neutral-300);
160
217
  --u-input-border-color-hover: var(--u-neutral-400);
161
- --u-input-border-color-focus: var(--u-blue-600);
162
- --u-input-border-color-invalid: var(--u-red-600);
218
+ --u-input-border-color-focus: var(--u-primary-color);
219
+ --u-input-border-color-invalid: var(--u-danger-color);
163
220
 
164
221
  /* Background Colors */
165
222
  --u-bg-color: var(--u-neutral-0);
@@ -0,0 +1,17 @@
1
+ /*
2
+ * 디자인 토큰 한 장 — 라이트 + 다크.
3
+ *
4
+ * import '@iyulab/components/styles/tokens.css';
5
+ *
6
+ * `Theme.init()` 을 부르지 않는 소비자를 위한 **정적 진입점**이다. 토큰이 없으면 컴포넌트
7
+ * 시트의 `var(--u-…)` 가 전부 무효가 되어 테두리·배경이 에러 없이 사라진다.
8
+ *
9
+ * 다크는 `:root[theme="dark"]` 로 스코프돼 있어 두 시트가 안전하게 공존한다 —
10
+ * `<html theme="dark">` 일 때만 활성화된다. 테마 전환/저장/시스템 감지가 필요하면
11
+ * `Theme.init()` 을 쓰면 되고, 그 경우 이 파일은 필요 없다(중복 정의는 무해하다).
12
+ *
13
+ * ⚠ 이 파일은 `Theme.init()` 이 인라인하는 번들에서 제외된다(Theme.ts 의 glob 참조) —
14
+ * 인라인된 `<style>` 안에서는 아래 상대 `@import` 가 문서 기준으로 해석돼 깨진다.
15
+ */
16
+ @import './light.css';
17
+ @import './dark.css';
@@ -5,6 +5,10 @@ import light_css_default from "../_virtual/_glob-assets_raw/light.css.637cb2f8.j
5
5
  /**
6
6
  * 스타일 시트 번들 로드, 내부 자산에서 CSS를 인라인으로 가져옵니다.
7
7
  * 빌드 시점에 정적 파일을 포함시키기 위해 vite의 `import.meta.glob`을 사용합니다.
8
+ *
9
+ * ⚠ 대상을 `{light,dark}` 로 **명시**한다. `*.css` 로 두면 정적 진입점용 `tokens.css`
10
+ * (light/dark 를 `@import` 하는 한 장)까지 인라인돼, `<style>` 안의 상대 `@import` 가
11
+ * 문서 기준으로 해석되면서 깨진다.
8
12
  */
9
13
  var internalStyleBundle = Object.entries(/* #__PURE__ */ Object.assign({
10
14
  "../assets/styles/dark.css": dark_css_default,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@iyulab/components",
3
3
  "description": "web-components library based on lit-element made by iyulab",
4
- "version": "1.9.0",
4
+ "version": "1.12.0",
5
5
  "keywords": [
6
6
  "iyulab",
7
7
  "components",
@@ -44,9 +44,12 @@
44
44
  "test:browser": "vitest run --project=browser",
45
45
  "lint": "eslint src/",
46
46
  "lint:fix": "eslint src/ --fix",
47
- "build:plugins": "tsc -p plugins/tsconfig.json",
48
- "build": "eslint && vite build && npm run build:plugins && npm run test:react-types",
49
- "test:react-types": "tsc -p tsconfig.react-smoke.json"
47
+ "build": "eslint && npm run typecheck:plugins && vite build && npm run test:react-types",
48
+ "test:react-types": "tsc -p tsconfig.react-smoke.json",
49
+ "typecheck:plugins": "tsc -p plugins/tsconfig.json",
50
+ "docs:cssprops": "node scripts/cssprops-doc.mjs --write",
51
+ "docs:react-events": "node scripts/react-events-doc.mjs --write",
52
+ "docs:tokens": "node scripts/design-tokens-doc.mjs --write"
50
53
  },
51
54
  "dependencies": {
52
55
  "@floating-ui/dom": "^1.8.0",
@@ -1,12 +0,0 @@
1
- import type { Plugin } from 'vite';
2
- /**
3
- * Rolldown의 preserveModules 모드에서 import.meta.glob의 query 옵션 사용 시
4
- * 빈 export가 생성되는 문제를 해결하는 플러그인.
5
- *
6
- * - `?raw`: 파일 내용을 문자열로 반환
7
- * - `?inline`: 파일 내용을 문자열로 반환
8
- * - `?url`: 파일을 asset으로 emit하고 URL을 반환
9
- *
10
- * 가상 모듈(\0 prefix)로 변환하여 파일명에 `?`가 포함되는 것을 방지합니다.
11
- */
12
- export default function globResolve(): Plugin;
@@ -1,66 +0,0 @@
1
- import { readFileSync } from 'fs';
2
- import { resolve, dirname, basename } from 'path';
3
- import { createHash } from 'crypto';
4
- const VIRTUAL_PREFIX = '\0glob-assets';
5
- const SUPPORTED_QUERIES = ['?raw', '?inline', '?url'];
6
- function shortHash(input) {
7
- return createHash('md5').update(input).digest('hex').slice(0, 8);
8
- }
9
- /**
10
- * Rolldown의 preserveModules 모드에서 import.meta.glob의 query 옵션 사용 시
11
- * 빈 export가 생성되는 문제를 해결하는 플러그인.
12
- *
13
- * - `?raw`: 파일 내용을 문자열로 반환
14
- * - `?inline`: 파일 내용을 문자열로 반환
15
- * - `?url`: 파일을 asset으로 emit하고 URL을 반환
16
- *
17
- * 가상 모듈(\0 prefix)로 변환하여 파일명에 `?`가 포함되는 것을 방지합니다.
18
- */
19
- export default function globResolve() {
20
- const idToAbsPath = new Map();
21
- return {
22
- name: 'vite:glob-resolve',
23
- enforce: 'pre',
24
- resolveId(source, importer) {
25
- for (const query of SUPPORTED_QUERIES) {
26
- if (source.endsWith(query)) {
27
- const rawPath = source.slice(0, -query.length);
28
- const absPath = importer
29
- ? resolve(dirname(importer), rawPath)
30
- : rawPath;
31
- const name = basename(absPath);
32
- const hash = shortHash(absPath);
33
- const queryName = query.slice(1);
34
- const virtualId = `${VIRTUAL_PREFIX}_${queryName}/${name}.${hash}`;
35
- idToAbsPath.set(virtualId, absPath);
36
- return virtualId;
37
- }
38
- }
39
- return null;
40
- },
41
- load(id) {
42
- if (!id.startsWith(VIRTUAL_PREFIX))
43
- return null;
44
- const absPath = idToAbsPath.get(id);
45
- if (!absPath)
46
- return null;
47
- // virtualId에서 queryName 추출: "\0glob-assets_{queryName}/..."
48
- const queryName = id.slice(VIRTUAL_PREFIX.length + 1).split('/')[0];
49
- if (queryName === 'raw' || queryName === 'inline') {
50
- const content = readFileSync(absPath, 'utf-8');
51
- return `export default ${JSON.stringify(content)};`;
52
- }
53
- if (queryName === 'url') {
54
- const source = readFileSync(absPath);
55
- const fileName = basename(absPath);
56
- const ref = this.emitFile({
57
- type: 'asset',
58
- name: fileName,
59
- source,
60
- });
61
- return `export default import.meta.ROLLUP_FILE_URL_${ref};`;
62
- }
63
- return null;
64
- }
65
- };
66
- }
@@ -1,14 +0,0 @@
1
- import type { Plugin } from 'vite';
2
- interface PluginOptions {
3
- /** 컴포넌트 소스 디렉토리 경로 (기본값: 'src/components') */
4
- input?: string;
5
- /** React 래퍼 출력 폴더 - 빌드 outDir 기준 상대 경로 (기본값: 'react') */
6
- output?: string;
7
- /** 래퍼 생성에서 제외할 glob 패턴 목록 (cwd: 프로젝트 루트) */
8
- exclude?: string[];
9
- }
10
- /**
11
- * Lit Element 컴포넌트를 React 래퍼로 자동 생성하는 Vite 플러그인
12
- */
13
- export default function reactWrapperPlugin(options: PluginOptions): Plugin;
14
- export {};
@@ -1,235 +0,0 @@
1
- import { resolve, join, relative, dirname, basename } from 'path';
2
- import { readFileSync, writeFileSync, mkdirSync } from 'fs';
3
- import { gzipSync } from 'zlib';
4
- import { globSync } from 'glob';
5
- /**
6
- * Lit Element 컴포넌트를 React 래퍼로 자동 생성하는 Vite 플러그인
7
- */
8
- export default function reactWrapperPlugin(options) {
9
- let rootDir;
10
- let buildOutDir;
11
- let outDir;
12
- return {
13
- name: 'vite-plugin-react-wrapper',
14
- configResolved(config) {
15
- rootDir = config.root;
16
- buildOutDir = resolve(rootDir, config.build.outDir);
17
- outDir = resolve(buildOutDir, options.output || 'react');
18
- },
19
- closeBundle() {
20
- const log = (msg) => console.log(`\x1b[36m[react-wrapper]\x1b[0m ${msg}`);
21
- log('Generating React wrappers...');
22
- // 컴포넌트 수집
23
- const inputPattern = (options.input || 'src/components') + '/**/*.ts';
24
- let files = globSync(inputPattern, { cwd: rootDir, absolute: true });
25
- if (options.exclude?.length) {
26
- const excluded = new Set(options.exclude.flatMap(p => globSync(p, { cwd: rootDir, absolute: true })));
27
- files = files.filter((f) => !excluded.has(f));
28
- }
29
- const components = files.flatMap((f) => parseComponent(f));
30
- if (components.length === 0) {
31
- log('No components found.');
32
- return;
33
- }
34
- // 출력 디렉토리 생성
35
- mkdirSync(outDir, { recursive: true });
36
- // 래퍼 생성
37
- const generated = [];
38
- for (const comp of components) {
39
- generated.push(...writeWrapper(comp, outDir, buildOutDir));
40
- }
41
- generated.push(...writeIndex(components, outDir, buildOutDir));
42
- // 결과 출력
43
- generated.sort((a, b) => a.size - b.size);
44
- for (const f of generated) {
45
- const kb = (f.size / 1024).toFixed(2);
46
- const gzKb = (f.gzipSize / 1024).toFixed(2);
47
- const pad = ' '.repeat(Math.max(0, 50 - f.path.length));
48
- console.log(`${f.path}${pad}${kb.padStart(6)} kB │ gzip: ${gzKb.padStart(6)} kB`);
49
- }
50
- log(`Generated ${components.length} React wrappers.`);
51
- }
52
- };
53
- }
54
- // --- 컴포넌트 파싱 ---
55
- function parseComponent(filePath) {
56
- const content = readFileSync(filePath, 'utf-8');
57
- // @customElement('tag-name') 데코레이터 확인
58
- const tagMatch = content.match(/@customElement\s*\(\s*['"]([^'"]+)['"]\s*\)/);
59
- if (!tagMatch)
60
- return [];
61
- // export class ClassName 추출
62
- const classMatch = content.match(/export\s+class\s+(\w+)/);
63
- if (!classMatch)
64
- return [];
65
- const events = collectEvents(content);
66
- // 이벤트를 발생시키지만(relay/dispatchEvent) 정적으로 이름을 도출할 수 없는 컴포넌트가
67
- // @event 태그도 없으면 events 맵이 빈 채로 생성되어 React 소비자가 이벤트를 구독할 수 없다.
68
- // (this.fire<T>('name')은 이름이 문자열 리터럴로 잡히므로 이 경고 대상이 아님)
69
- if (events.length === 0 && /this\.(relay|dispatchEvent)\s*\(/.test(content)) {
70
- console.warn(`\x1b[33m[react-wrapper]\x1b[0m ${classMatch[1]} (${tagMatch[1]}): ` +
71
- `이벤트를 발생시키지만(relay/dispatchEvent) @event 태그가 없어 React 래퍼에 이벤트 prop이 노출되지 않습니다. ` +
72
- `클래스 JSDoc에 '@event <name>'을 추가하세요.`);
73
- }
74
- return [{
75
- className: classMatch[1],
76
- tagName: tagMatch[1],
77
- filePath,
78
- events,
79
- }];
80
- }
81
- function collectEvents(content) {
82
- // import 구문에서 타입명 → 소스 경로 매핑 수집
83
- // 예: import { ShowEventDetail } from '../../events/ShowEvent.js';
84
- const typeImportMap = new Map();
85
- for (const m of content.matchAll(/import\s+\{([^}]+)\}\s+from\s+['"]([^'"]+)['"]/g)) {
86
- const importPath = m[2];
87
- for (const name of m[1].split(',')) {
88
- const typeName = name.replace(/type\s+/, '').trim();
89
- if (typeName)
90
- typeImportMap.set(typeName, importPath);
91
- }
92
- }
93
- const eventMap = new Map();
94
- // JSDoc @event 패턴: @event eventName
95
- for (const m of content.matchAll(/@event\s+([\w-]+)/g)) {
96
- if (!eventMap.has(m[1]))
97
- eventMap.set(m[1], { detailType: 'unknown', importPath: '' });
98
- }
99
- // this.fire<DetailType>('eventName') 패턴 - 제네릭 타입 추출
100
- for (const m of content.matchAll(/this\.fire\s*(?:<([^>]+)>)?\s*\(\s*['"]([\w-]+)['"]/g)) {
101
- const detailType = m[1] || 'unknown';
102
- const importPath = typeImportMap.get(detailType) || '';
103
- eventMap.set(m[2], { detailType, importPath });
104
- }
105
- return Array.from(eventMap, ([name, { detailType, importPath }]) => ({
106
- name,
107
- reactName: toCamelEvent(name),
108
- detailType,
109
- importPath,
110
- }));
111
- }
112
- /** kebab-case 이벤트명을 onCamelCase로 변환 (예: shift-start → onShiftStart) */
113
- function toCamelEvent(name) {
114
- return 'on' + name.split('-').map(s => s.charAt(0).toUpperCase() + s.slice(1)).join('');
115
- }
116
- function writeFile(filePath, content, buildOutDir) {
117
- writeFileSync(filePath, content, 'utf-8');
118
- return {
119
- path: relative(buildOutDir, filePath).replace(/\\/g, '/'),
120
- size: Buffer.byteLength(content, 'utf-8'),
121
- gzipSize: gzipSync(content).length,
122
- };
123
- }
124
- function computeImportPath(from, to) {
125
- let rel = relative(dirname(from), to).replace(/\\/g, '/').replace(/\.ts$/, '');
126
- if (!rel.startsWith('.'))
127
- rel = './' + rel;
128
- return rel;
129
- }
130
- function writeWrapper(comp, outDir, buildOutDir) {
131
- const { className, tagName, events } = comp;
132
- const jsPath = join(outDir, `${className}.js`);
133
- const dtsPath = join(outDir, `${className}.d.ts`);
134
- // 소스 파일의 빌드 출력 경로 추정 (src/ → 빌드 outDir 매핑)
135
- const srcIndex = comp.filePath.replace(/\\/g, '/').indexOf('/src/');
136
- const relFromSrc = srcIndex >= 0
137
- ? comp.filePath.substring(srcIndex + 5).replace(/\\/g, '/').replace(/\.ts$/, '')
138
- : basename(comp.filePath, '.ts');
139
- const builtModulePath = resolve(buildOutDir, relFromSrc + '.js');
140
- const importPath = computeImportPath(jsPath, builtModulePath);
141
- // events 객체 생성
142
- const eventsObj = events.length > 0
143
- ? `{\n${events.map(e => ` ${e.reactName}: '${e.name}',`).join('\n')}\n }`
144
- : '{}';
145
- // .js
146
- const js = `import React from 'react';
147
- import { createComponent } from '@lit/react';
148
- import { ${className} as ${className}Element } from '${importPath}';
149
-
150
- export const ${className} = createComponent({
151
- react: React,
152
- tagName: '${tagName}',
153
- elementClass: ${className}Element,
154
- events: ${eventsObj},
155
- });
156
- `;
157
- // .d.ts - import 구문 생성
158
- const dtsImportPath = importPath.replace(/\.js$/, '');
159
- const dtsImports = [
160
- `import React from 'react';`,
161
- `import { ${className} as ${className}Element } from '${dtsImportPath}';`,
162
- ];
163
- // 이벤트 detail 타입별 import - 소스 원본 경로 기준으로 빌드 경로 계산
164
- const srcDir = dirname(comp.filePath);
165
- const detailImportMap = new Map(); // 빌드경로 → [타입명]
166
- for (const e of events) {
167
- if (e.detailType === 'unknown' || !e.importPath)
168
- continue;
169
- // 소스 상대경로를 절대경로로 변환 후 빌드 경로 계산
170
- const absEventSrc = resolve(srcDir, e.importPath).replace(/\\/g, '/');
171
- const eventSrcIndex = absEventSrc.indexOf('/src/');
172
- const relFromSrc = eventSrcIndex >= 0
173
- ? absEventSrc.substring(eventSrcIndex + 5).replace(/\.js$/, '')
174
- : '';
175
- if (!relFromSrc)
176
- continue;
177
- const absEventBuilt = resolve(buildOutDir, relFromSrc);
178
- const eventImportPath = computeImportPath(dtsPath, absEventBuilt + '.js').replace(/\.js$/, '');
179
- const types = detailImportMap.get(eventImportPath) || [];
180
- if (!types.includes(e.detailType))
181
- types.push(e.detailType);
182
- detailImportMap.set(eventImportPath, types);
183
- }
184
- for (const [path, types] of detailImportMap) {
185
- dtsImports.push(`import { type ${types.join(', type ')} } from '${path}';`);
186
- }
187
- const eventTypes = events.length > 0
188
- ? events.map(e => {
189
- const type = e.detailType !== 'unknown' ? `CustomEvent<${e.detailType}>` : 'CustomEvent';
190
- return ` ${e.reactName}?: (event: ${type}) => void;`;
191
- }).join('\n') + '\n'
192
- : '';
193
- // 래퍼 이벤트 prop(onChange 등)은 React.HTMLAttributes 의 동명 핸들러와 교집합되면
194
- // 어떤 시그니처도 대입 불가가 된다. HTMLAttributes 쪽에서 해당 키를 제거해 CustomEvent
195
- // 시그니처만 남긴다. 이벤트가 없으면 Omit<…, never> 로 HTMLAttributes 원형 유지.
196
- const eventKeyUnion = events.length > 0
197
- ? events.map(e => `'${e.reactName}'`).join(' | ')
198
- : 'never';
199
- // Partial<Element> 은 DOM 프로퍼티(children: HTMLCollection 등)를 포함해 React 의 JSX
200
- // children/이벤트/className 등과 충돌한다. keyof React.HTMLAttributes 를 제거해 컴포넌트
201
- // 고유 prop만 남기고, React 친화 타입(children: ReactNode 포함)은 HTMLAttributes 가 제공한다.
202
- //
203
- // `React.RefAttributes` 를 반드시 교집합에 넣는다 — `ForwardRefExoticComponent<P>` 는 P 에
204
- // ref 를 자동으로 더해 주지 않는다. @lit/react 의 `ReactWebComponent` 자신이
205
- // `PropsWithoutRef<…> & React.RefAttributes<I>` 로 선언돼 있으므로(런타임은 ref 를 전달한다),
206
- // 이것을 빠뜨리면 **런타임은 되는데 타입만 거부하는** 괴리가 생긴다.
207
- const dts = `${dtsImports.join('\n')}
208
-
209
- export declare const ${className}: React.ForwardRefExoticComponent<
210
- Omit<Partial<${className}Element>, keyof React.HTMLAttributes<${className}Element>>
211
- & Omit<React.HTMLAttributes<${className}Element>, ${eventKeyUnion}>
212
- & React.RefAttributes<${className}Element>
213
- & {
214
- ${eventTypes} }
215
- >;
216
-
217
- export type ${className}Props = React.ComponentProps<typeof ${className}>;
218
- `;
219
- return [
220
- writeFile(jsPath, js, buildOutDir),
221
- writeFile(dtsPath, dts, buildOutDir),
222
- ];
223
- }
224
- function writeIndex(components, outDir, buildOutDir) {
225
- const jsExports = components
226
- .map(c => `export { ${c.className} } from './${c.className}.js';`)
227
- .join('\n');
228
- const dtsExports = components
229
- .map(c => `export { ${c.className}, ${c.className}Props } from './${c.className}';`)
230
- .join('\n');
231
- return [
232
- writeFile(join(outDir, 'index.js'), jsExports + '\n', buildOutDir),
233
- writeFile(join(outDir, 'index.d.ts'), dtsExports + '\n', buildOutDir),
234
- ];
235
- }