@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.
@@ -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 Inclusion
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 <head>/<body> of the final HTML
61
- - When **js.required is true**, include the CDN JS <script> tag. This is mandatory.
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
- "children": "모달 내용입니다.",
56
- "size": "md"
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
- "children": "정말 삭제하시겠습니까?",
61
- "size": "sm"
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
- "dropdown select",
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 우선",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ncds/ui-admin-mcp",
3
- "version": "1.0.0-alpha.10",
3
+ "version": "1.0.0-alpha.11",
4
4
  "description": "NCDS UI Admin MCP 서버 — AI 에이전트가 NCUA 컴포넌트를 조회하고 HTML을 검증할 수 있는 MCP 서버",
5
5
  "bin": {
6
6
  "ncua-mcp": "./bin/server.mjs"