@ncds/ui-admin-mcp 1.0.0-alpha.10 → 1.0.0-alpha.11
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/bin/definitions/instructions.md +103 -3
- package/bin/tools/renderToHtml.js +65 -0
- package/data/modal.json +78 -4
- package/data/select.json +8 -20
- package/package.json +1 -1
|
@@ -54,11 +54,30 @@ You are an agent that builds UI using NCUA (NCDS UI Admin) design system compone
|
|
|
54
54
|
2. Retry render_to_html with meaningful prop values
|
|
55
55
|
3. If still empty after 3 attempts, report the issue to the user
|
|
56
56
|
|
|
57
|
-
### Step 4: CDN
|
|
57
|
+
### Step 4: CDN & JS Initialization
|
|
58
58
|
|
|
59
59
|
- Get CSS/JS URLs from the **cdn** field in the render_to_html response
|
|
60
|
-
- Include them in
|
|
61
|
-
- When **js.required is true
|
|
60
|
+
- Include them in `<head>`/`<body>` of the final HTML
|
|
61
|
+
- When **js.required is true**:
|
|
62
|
+
1. Include the CDN JS `<script>` tag — this is mandatory
|
|
63
|
+
2. Check the **js.api** field — it contains the initialization code pattern
|
|
64
|
+
3. Write a `<script>` block that initializes the component using `js.api.constructor` and `js.api.constructorParams`
|
|
65
|
+
4. Use `js.api.example` as a reference for the initialization code
|
|
66
|
+
5. Common pattern: `new window.ncua.ClassName(element, options)` — called after DOMContentLoaded
|
|
67
|
+
|
|
68
|
+
Example: DatePicker with JS initialization:
|
|
69
|
+
|
|
70
|
+
```html
|
|
71
|
+
<script>
|
|
72
|
+
document.addEventListener('DOMContentLoaded', function () {
|
|
73
|
+
// js.api.constructor: new window.ncua.DatePicker(wrapper, options)
|
|
74
|
+
// js.api.constructorParams shows required params
|
|
75
|
+
new window.ncua.DatePicker(document.getElementById('my-datepicker'), {
|
|
76
|
+
datePickerOptions: [{ element: 'start-date', options: { dateFormat: 'Y-m-d' } }],
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
</script>
|
|
80
|
+
```
|
|
62
81
|
|
|
63
82
|
### Step 5: Composition
|
|
64
83
|
|
|
@@ -124,8 +143,89 @@ If you call `render_to_html` with missing required props:
|
|
|
124
143
|
- File upload → **file-input**
|
|
125
144
|
- Image upload → **image-file-input**
|
|
126
145
|
- Plain text → **input**
|
|
146
|
+
- Dropdown/Select → **select-box** (default choice for all dropdown selections). Use **select** only when native HTML select is explicitly required (e.g., react-hook-form direct binding)
|
|
147
|
+
- Searchable dropdown → **combo-box**
|
|
148
|
+
- Custom trigger dropdown → **dropdown**
|
|
149
|
+
- Date single → **date-picker**
|
|
150
|
+
- Date range → **range-date-picker**
|
|
151
|
+
- Date range with period buttons → **range-date-picker-with-buttons**
|
|
127
152
|
- Each component's description includes usage guidance and alternatives.
|
|
128
153
|
|
|
154
|
+
### Icon Usage
|
|
155
|
+
|
|
156
|
+
Icons are rendered as inline SVG by the MCP server — NOT via CDN JS or custom tags. There is NO `<ncua-icon>` tag.
|
|
157
|
+
|
|
158
|
+
- Use `search_icon` to find icon names (PascalCase, e.g., "RefreshCw01", "SearchLg", "ChevronDown")
|
|
159
|
+
- Pass icon names to component props via `render_to_html`:
|
|
160
|
+
- Button leadingIcon/trailingIcon: `{ "type": "icon", "icon": "RefreshCw01" }`
|
|
161
|
+
- FeaturedIcon icon: `"CheckCircle"` (bare string)
|
|
162
|
+
- The server resolves icon names to actual SVG at render time
|
|
163
|
+
- NEVER write `<ncua-icon>`, `<svg>`, or emoji. If you need a standalone icon, use **featured-icon** component
|
|
164
|
+
|
|
165
|
+
Example — Button with icon:
|
|
166
|
+
|
|
167
|
+
```json
|
|
168
|
+
{
|
|
169
|
+
"name": "button",
|
|
170
|
+
"props": {
|
|
171
|
+
"label": "Refresh",
|
|
172
|
+
"leadingIcon": { "type": "icon", "icon": "RefreshCw01" }
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Children Composition
|
|
178
|
+
|
|
179
|
+
When a component needs child components (e.g., ButtonGroup with Buttons, Modal with form elements), pass children as component descriptors:
|
|
180
|
+
|
|
181
|
+
```json
|
|
182
|
+
{
|
|
183
|
+
"name": "button-group",
|
|
184
|
+
"props": {
|
|
185
|
+
"children": [
|
|
186
|
+
{ "component": "button", "props": { "label": "Save", "hierarchy": "primary" } },
|
|
187
|
+
{ "component": "button", "props": { "label": "Cancel" } }
|
|
188
|
+
]
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
This works recursively — children can contain their own children up to 5 levels deep.
|
|
194
|
+
|
|
195
|
+
### Compound Components (dot notation)
|
|
196
|
+
|
|
197
|
+
Some components have sub-components accessed via dot notation. Use `"parent.sub"` format:
|
|
198
|
+
|
|
199
|
+
- `modal.header` — Modal header with title and close button
|
|
200
|
+
- `modal.content` — Modal body content area
|
|
201
|
+
- `modal.actions` — Modal footer with action buttons
|
|
202
|
+
|
|
203
|
+
Example — Modal with full structure:
|
|
204
|
+
|
|
205
|
+
```json
|
|
206
|
+
{
|
|
207
|
+
"name": "modal",
|
|
208
|
+
"props": { "isOpen": true, "size": "md" },
|
|
209
|
+
"children": [
|
|
210
|
+
{ "component": "modal.header", "props": { "title": "Product Detail", "align": "left" } },
|
|
211
|
+
{
|
|
212
|
+
"component": "modal.content",
|
|
213
|
+
"children": [{ "component": "input-base", "props": { "placeholder": "Product name" } }]
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
"component": "modal.actions",
|
|
217
|
+
"props": { "layout": "horizontal", "align": "stretch" },
|
|
218
|
+
"children": [
|
|
219
|
+
{ "component": "button", "props": { "label": "Cancel", "hierarchy": "secondary-gray", "size": "sm" } },
|
|
220
|
+
{ "component": "button", "props": { "label": "Confirm", "hierarchy": "primary", "size": "sm" } }
|
|
221
|
+
]
|
|
222
|
+
}
|
|
223
|
+
]
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Always check `usageExamples` in `get_component_props` for compound component patterns. You can freely omit or reorder sub-components to customize the layout.
|
|
228
|
+
|
|
129
229
|
## Rules Schema
|
|
130
230
|
|
|
131
231
|
The `rules` array in ping response is a flat list of strings grouped by category. Categories:
|
|
@@ -7,12 +7,72 @@ const response_js_1 = require("../utils/response.js");
|
|
|
7
7
|
const BLOCKED_PROPS = new Set(['dangerouslySetInnerHTML', 'ref', '__self', '__source']);
|
|
8
8
|
/** 아이콘 관련 props — React 변환 시 import 생성 대상 */
|
|
9
9
|
const ICON_PROP_NAMES = new Set(['leadingIcon', 'trailingIcon', 'icon', 'groupIcon']);
|
|
10
|
+
/** children 중첩 렌더링 제한 */
|
|
11
|
+
const MAX_CHILDREN_DEPTH = 5;
|
|
12
|
+
const MAX_CHILDREN_COUNT = 30;
|
|
10
13
|
// ── 타입 가드 ──────────────────────────────────────────────────────
|
|
11
14
|
/** { type: 'icon', icon: string } 형태인지 판별 */
|
|
12
15
|
const isIconSlot = (value) => typeof value === 'object' &&
|
|
13
16
|
value !== null &&
|
|
14
17
|
value.type === 'icon' &&
|
|
15
18
|
typeof value.icon === 'string';
|
|
19
|
+
// ── 타입 가드: children 중첩 ────────────────────────────────────────
|
|
20
|
+
/** { component: string, props?: object } 형태인지 판별 */
|
|
21
|
+
const isChildDescriptor = (value) => typeof value === 'object' && value !== null && typeof value.component === 'string';
|
|
22
|
+
/** PascalCase 변환: "header" → "Header" */
|
|
23
|
+
const capitalize = (s) => s.charAt(0).toUpperCase() + s.slice(1);
|
|
24
|
+
/** 단순 컴포넌트 resolve: "button" → bundle.Button */
|
|
25
|
+
const resolveSimpleComponent = (name, ctx) => {
|
|
26
|
+
const normalized = (0, response_js_1.normalizeName)(name);
|
|
27
|
+
const childData = ctx.componentMap.get(normalized);
|
|
28
|
+
return childData?.exportName ? ctx.bundle[childData.exportName] ?? null : null;
|
|
29
|
+
};
|
|
30
|
+
/** compound component resolve: "modal.header" → bundle.Modal.Header */
|
|
31
|
+
const resolveCompoundComponent = (name, dotIndex, ctx) => {
|
|
32
|
+
const parentName = name.slice(0, dotIndex).toLowerCase();
|
|
33
|
+
const subName = name.slice(dotIndex + 1);
|
|
34
|
+
const parentData = ctx.componentMap.get(parentName);
|
|
35
|
+
const parentExportName = parentData?.exportName;
|
|
36
|
+
if (!parentExportName)
|
|
37
|
+
return null;
|
|
38
|
+
const Parent = ctx.bundle[parentExportName];
|
|
39
|
+
return Parent?.[capitalize(subName)] ?? null;
|
|
40
|
+
};
|
|
41
|
+
/** children JSON을 재귀적으로 React element로 변환 */
|
|
42
|
+
const resolveChildren = (children, ctx, depth) => {
|
|
43
|
+
if (depth > MAX_CHILDREN_DEPTH)
|
|
44
|
+
return null;
|
|
45
|
+
// 문자열/숫자/boolean — 그대로 반환
|
|
46
|
+
if (typeof children === 'string' || typeof children === 'number' || typeof children === 'boolean')
|
|
47
|
+
return children;
|
|
48
|
+
// 배열 — 각 요소를 재귀 처리 (MAX_CHILDREN_COUNT 제한)
|
|
49
|
+
if (Array.isArray(children)) {
|
|
50
|
+
return children.slice(0, MAX_CHILDREN_COUNT).map((child) => resolveChildren(child, ctx, depth));
|
|
51
|
+
}
|
|
52
|
+
// { component, props?, children? } — React element로 변환
|
|
53
|
+
if (isChildDescriptor(children)) {
|
|
54
|
+
const componentName = children.component.trim();
|
|
55
|
+
const dotIndex = componentName.indexOf('.');
|
|
56
|
+
// dot notation: "modal.header" → bundle.Modal.Header
|
|
57
|
+
const Component = dotIndex > 0
|
|
58
|
+
? resolveCompoundComponent(componentName, dotIndex, ctx)
|
|
59
|
+
: resolveSimpleComponent(componentName, ctx);
|
|
60
|
+
if (!Component)
|
|
61
|
+
return null;
|
|
62
|
+
const childProps = { ...(children.props ?? {}) };
|
|
63
|
+
// descriptor의 children 필드가 있으면 props.children보다 우선
|
|
64
|
+
const rawChildren = children.children !== undefined ? children.children : childProps.children;
|
|
65
|
+
if (rawChildren !== undefined) {
|
|
66
|
+
childProps.children = resolveChildren(rawChildren, ctx, depth + 1);
|
|
67
|
+
}
|
|
68
|
+
// dot notation은 componentMap에 props spec이 없으므로 icon resolve만 적용
|
|
69
|
+
const parentName = dotIndex > 0 ? componentName.slice(0, dotIndex).toLowerCase() : componentName.toLowerCase();
|
|
70
|
+
const childData = ctx.componentMap.get(parentName);
|
|
71
|
+
const resolvedProps = resolveIconProps(childProps, ctx.iconBundle, childData?.props);
|
|
72
|
+
return ctx.reactRuntime.createElement(Component, resolvedProps);
|
|
73
|
+
}
|
|
74
|
+
return children;
|
|
75
|
+
};
|
|
16
76
|
// ── Props 변환 ──────────────────────────────────────────────────────
|
|
17
77
|
/** BLOCKED_PROPS를 제거 — props spec 유무와 무관하게 항상 적용 */
|
|
18
78
|
const removeBlockedProps = (props) => Object.fromEntries(Object.entries(props).filter(([key]) => !BLOCKED_PROPS.has(key)));
|
|
@@ -238,6 +298,11 @@ const renderToHtml = (params) => {
|
|
|
238
298
|
const postWarnings = componentData.props ? validateProps(corrected, componentData.props) : [];
|
|
239
299
|
const warnings = [...enumWarnings, ...postWarnings.filter((w) => !enumWarnings.includes(w))];
|
|
240
300
|
const resolvedProps = resolveIconProps(corrected, iconBundle, componentData.props);
|
|
301
|
+
// children이 컴포넌트 descriptor면 React element로 변환
|
|
302
|
+
if (resolvedProps.children !== undefined) {
|
|
303
|
+
const ctx = { bundle, iconBundle, componentMap, reactRuntime };
|
|
304
|
+
resolvedProps.children = resolveChildren(resolvedProps.children, ctx, 0);
|
|
305
|
+
}
|
|
241
306
|
const rawHtml = reactRuntime.renderToStaticMarkup(reactRuntime.createElement(Component, resolvedProps));
|
|
242
307
|
const html = `<!-- ncua:${normalized} start -->\n${rawHtml}\n<!-- ncua:${normalized} end -->`;
|
|
243
308
|
const hasDefaults = componentData.props ? calcDefaultsUsed(componentData.props, corrected) : {};
|
package/data/modal.json
CHANGED
|
@@ -52,13 +52,87 @@
|
|
|
52
52
|
"usageExamples": {
|
|
53
53
|
"default": {
|
|
54
54
|
"isOpen": true,
|
|
55
|
-
"
|
|
56
|
-
"
|
|
55
|
+
"size": "md",
|
|
56
|
+
"children": [
|
|
57
|
+
{
|
|
58
|
+
"component": "modal.header",
|
|
59
|
+
"props": {
|
|
60
|
+
"title": "제목",
|
|
61
|
+
"align": "left"
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"component": "modal.content",
|
|
66
|
+
"children": "모달 내용입니다."
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"component": "modal.actions",
|
|
70
|
+
"props": {
|
|
71
|
+
"layout": "horizontal",
|
|
72
|
+
"align": "stretch"
|
|
73
|
+
},
|
|
74
|
+
"children": [
|
|
75
|
+
{
|
|
76
|
+
"component": "button",
|
|
77
|
+
"props": {
|
|
78
|
+
"label": "취소",
|
|
79
|
+
"hierarchy": "secondary-gray",
|
|
80
|
+
"size": "sm"
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"component": "button",
|
|
85
|
+
"props": {
|
|
86
|
+
"label": "확인",
|
|
87
|
+
"hierarchy": "primary",
|
|
88
|
+
"size": "sm"
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
]
|
|
92
|
+
}
|
|
93
|
+
]
|
|
57
94
|
},
|
|
58
95
|
"confirm": {
|
|
59
96
|
"isOpen": true,
|
|
60
|
-
"
|
|
61
|
-
"
|
|
97
|
+
"size": "sm",
|
|
98
|
+
"children": [
|
|
99
|
+
{
|
|
100
|
+
"component": "modal.header",
|
|
101
|
+
"props": {
|
|
102
|
+
"title": "삭제 확인",
|
|
103
|
+
"align": "center"
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"component": "modal.content",
|
|
108
|
+
"children": "정말 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다."
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"component": "modal.actions",
|
|
112
|
+
"props": {
|
|
113
|
+
"layout": "vertical",
|
|
114
|
+
"align": "stretch"
|
|
115
|
+
},
|
|
116
|
+
"children": [
|
|
117
|
+
{
|
|
118
|
+
"component": "button",
|
|
119
|
+
"props": {
|
|
120
|
+
"label": "삭제",
|
|
121
|
+
"hierarchy": "destructive",
|
|
122
|
+
"size": "sm"
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"component": "button",
|
|
127
|
+
"props": {
|
|
128
|
+
"label": "취소",
|
|
129
|
+
"hierarchy": "secondary-gray",
|
|
130
|
+
"size": "sm"
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
]
|
|
134
|
+
}
|
|
135
|
+
]
|
|
62
136
|
}
|
|
63
137
|
},
|
|
64
138
|
"props": {
|
package/data/select.json
CHANGED
|
@@ -4,30 +4,18 @@
|
|
|
4
4
|
"importPath": "@ncds/ui-admin",
|
|
5
5
|
"jsRequired": false,
|
|
6
6
|
"category": "input",
|
|
7
|
-
"description": "네이티브 HTML select
|
|
7
|
+
"description": "네이티브 HTML select 래퍼입니다. 대부분의 경우 select-box를 사용하세요. 네이티브 select가 명시적으로 필요한 경우에만 사용합니다.",
|
|
8
8
|
"aliases": [
|
|
9
|
-
"셀렉트",
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"정렬",
|
|
15
|
-
"필터",
|
|
16
|
-
"Select",
|
|
17
|
-
"SelectBox",
|
|
18
|
-
"셀렉트박스",
|
|
19
|
-
"Dropdown",
|
|
20
|
-
"드롭다운",
|
|
21
|
-
"옵션선택",
|
|
22
|
-
"placeholder",
|
|
23
|
-
"react-hook-form",
|
|
24
|
-
"Single Select"
|
|
9
|
+
"네이티브 셀렉트",
|
|
10
|
+
"native select",
|
|
11
|
+
"HTML select",
|
|
12
|
+
"기본 선택",
|
|
13
|
+
"react-hook-form"
|
|
25
14
|
],
|
|
26
15
|
"hasChildren": true,
|
|
27
16
|
"whenToUse": [
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"옵션 수 6개 이상이거나 공간이 제한적인 경우"
|
|
17
|
+
"네이티브 HTML select가 명시적으로 필요한 경우만",
|
|
18
|
+
"react-hook-form 등 폼 라이브러리와 직접 연동이 필요한 경우"
|
|
31
19
|
],
|
|
32
20
|
"forbiddenRules": [
|
|
33
21
|
"옵션 2개(반대 의미)이면 Switch 우선",
|