Skip to main content
Glama
ncds-component-mapper.ts16.3 kB
import type { SimplifiedNode } from "~/extractors/types.js"; export interface NcdsComponentMapping { component: string; props: Record<string, any>; htmlTag: string; className: string; children?: NcdsComponentMapping[]; } export interface ComponentMappingRule { condition: (node: SimplifiedNode) => boolean; mapper: (node: SimplifiedNode) => NcdsComponentMapping; } // NCDS UI Admin 컴포넌트 매핑 규칙들 export const NCDS_COMPONENT_RULES: ComponentMappingRule[] = [ // Button 매핑 { condition: (node) => { const name = node.name.toLowerCase(); return ( node.type === 'INSTANCE' || (node.type === 'FRAME' && (name.includes('button') || name.includes('btn') || name.includes('click') || name.includes('submit'))) ); }, mapper: (node) => ({ component: 'Button', props: { label: node.text || node.name, hierarchy: inferButtonTheme(node), size: inferSize(node), disabled: node.name.toLowerCase().includes('disabled') }, htmlTag: 'button', className: 'ncua-btn' }) }, // Input 매핑 { condition: (node) => { const name = node.name.toLowerCase(); return node.type === 'FRAME' && (name.includes('input') || name.includes('field') || name.includes('text') && !name.includes('label')); }, mapper: (node) => ({ component: 'InputBase', props: { placeholder: node.text || 'Enter text...', size: inferSize(node), disabled: node.name.toLowerCase().includes('disabled'), required: node.name.toLowerCase().includes('required') }, htmlTag: 'input', className: 'ncua-input' }) }, // Checkbox 매핑 { condition: (node) => { const name = node.name.toLowerCase(); return node.type === 'FRAME' && (name.includes('checkbox') || name.includes('check')); }, mapper: (node) => ({ component: 'Checkbox', props: { label: node.text || node.name, checked: node.name.toLowerCase().includes('checked'), disabled: node.name.toLowerCase().includes('disabled') }, htmlTag: 'label', className: 'ncua-checkbox' }) }, // Radio 매핑 { condition: (node) => { const name = node.name.toLowerCase(); return node.type === 'FRAME' && name.includes('radio'); }, mapper: (node) => ({ component: 'Radio', props: { label: node.text || node.name, checked: node.name.toLowerCase().includes('selected'), disabled: node.name.toLowerCase().includes('disabled') }, htmlTag: 'label', className: 'ncua-radio' }) }, // Select/Dropdown 매핑 { condition: (node) => { const name = node.name.toLowerCase(); return node.type === 'FRAME' && (name.includes('select') || name.includes('dropdown') || name.includes('combo')); }, mapper: (node) => ({ component: 'Select', props: { placeholder: node.text || 'Select an option', size: inferSize(node), disabled: node.name.toLowerCase().includes('disabled') }, htmlTag: 'select', className: 'ncua-select' }) }, // Badge 매핑 { condition: (node) => { const name = node.name.toLowerCase(); return (node.type === 'FRAME' || node.type === 'TEXT') && (name.includes('badge') || name.includes('tag') || name.includes('chip')); }, mapper: (node) => ({ component: 'Badge', props: { label: node.text || node.name, color: inferBadgeColor(node), size: inferSize(node) }, htmlTag: 'span', className: 'ncua-badge' }) }, // Modal 매핑 { condition: (node) => { const name = node.name.toLowerCase(); return node.type === 'FRAME' && (name.includes('modal') || name.includes('dialog') || name.includes('popup')); }, mapper: (node) => ({ component: 'Modal', props: { isOpen: true, title: findChildText(node, 'title') || 'Modal Title' }, htmlTag: 'div', className: 'ncua-modal' }) }, // Tab 매핑 { condition: (node) => { const name = node.name.toLowerCase(); return node.type === 'FRAME' && (name.includes('tab') && !name.includes('table')); }, mapper: (node) => ({ component: 'HorizontalTab', props: { tabs: extractTabItems(node) }, htmlTag: 'div', className: 'ncua-tab' }) }, // Pagination 매핑 { condition: (node) => { const name = node.name.toLowerCase(); return node.type === 'FRAME' && (name.includes('pagination') || name.includes('pager')); }, mapper: (node) => ({ component: 'Pagination', props: { currentPage: 1, totalPages: 10, size: inferSize(node) }, htmlTag: 'nav', className: 'ncua-pagination' }) }, // Progress Bar 매핑 { condition: (node) => { const name = node.name.toLowerCase(); return node.type === 'FRAME' && (name.includes('progress') && name.includes('bar')); }, mapper: (node) => ({ component: 'ProgressBar', props: { progress: 50, size: inferSize(node) }, htmlTag: 'div', className: 'ncua-progress-bar' }) }, // Notification 매핑 { condition: (node) => { const name = node.name.toLowerCase(); return node.type === 'FRAME' && (name.includes('notification') || name.includes('alert') || name.includes('toast')); }, mapper: (node) => ({ component: 'Notification', props: { title: findChildText(node, 'title') || 'Notification', description: findChildText(node, 'description') || node.text, type: inferNotificationType(node) }, htmlTag: 'div', className: 'ncua-notification' }) }, // VerticalTab 매핑 { condition: (node) => { const name = node.name.toLowerCase(); return node.type === 'FRAME' && (name.includes('vertical') && name.includes('tab')); }, mapper: (node) => ({ component: 'VerticalTab', props: { tabs: extractTabItems(node), type: 'button-primary' }, htmlTag: 'div', className: 'ncua-vertical-tab' }) }, // ProgressCircle 매핑 { condition: (node) => { const name = node.name.toLowerCase(); return node.type === 'FRAME' && (name.includes('progress') && (name.includes('circle') || name.includes('circular'))); }, mapper: (node) => ({ component: 'ProgressCircle', props: { progress: 50, size: inferSize(node) }, htmlTag: 'div', className: 'ncua-progress-circle' }) }, // Spinner 매핑 { condition: (node) => { const name = node.name.toLowerCase(); return node.type === 'FRAME' && (name.includes('spinner') || name.includes('loading') || name.includes('loader')); }, mapper: (node) => ({ component: 'Spinner', props: { size: inferSize(node), text: node.text }, htmlTag: 'div', className: 'ncua-spinner' }) }, // Tag 매핑 { condition: (node) => { const name = node.name.toLowerCase(); return (node.type === 'FRAME' || node.type === 'TEXT') && (name.includes('tag') && !name.includes('stage')); }, mapper: (node) => ({ component: 'Tag', props: { label: node.text || node.name, color: inferTagColor(node), size: inferSize(node) }, htmlTag: 'span', className: 'ncua-tag' }) }, // Toggle 매핑 { condition: (node) => { const name = node.name.toLowerCase(); return node.type === 'FRAME' && (name.includes('toggle') || name.includes('switch')); }, mapper: (node) => ({ component: 'Toggle', props: { checked: node.name.toLowerCase().includes('on'), disabled: node.name.toLowerCase().includes('disabled'), size: inferSize(node), text: node.text }, htmlTag: 'label', className: 'ncua-toggle' }) }, // Tooltip 매핑 { condition: (node) => { const name = node.name.toLowerCase(); return node.type === 'FRAME' && name.includes('tooltip'); }, mapper: (node) => ({ component: 'Tooltip', props: { title: findChildText(node, 'title') || node.text, position: 'top' }, htmlTag: 'span', className: 'ncua-tooltip' }) }, // Slider 매핑 { condition: (node) => { const name = node.name.toLowerCase(); return node.type === 'FRAME' && (name.includes('slider') || name.includes('range')); }, mapper: (node) => ({ component: 'Slider', props: { value: 50, min: 0, max: 100, size: inferSize(node) }, htmlTag: 'div', className: 'ncua-slider' }) }, // BreadCrumb 매핑 { condition: (node) => { const name = node.name.toLowerCase(); return node.type === 'FRAME' && (name.includes('breadcrumb') || name.includes('bread')); }, mapper: (node) => ({ component: 'BreadCrumb', props: { items: [{ label: 'Home', href: '/' }, { label: 'Current', href: '#' }] }, htmlTag: 'nav', className: 'ncua-breadcrumb' }) }, // Divider 매핑 { condition: (node) => { const name = node.name.toLowerCase(); return (node.type === 'FRAME' || node.type === 'LINE') && (name.includes('divider') || name.includes('separator') || name.includes('line')); }, mapper: (node) => ({ component: 'Divider', props: { orientation: node.layout?.width > node.layout?.height ? 'horizontal' : 'vertical', text: node.text }, htmlTag: 'hr', className: 'ncua-divider' }) }, // Dropdown 매핑 { condition: (node) => { const name = node.name.toLowerCase(); return node.type === 'FRAME' && (name.includes('dropdown') && !name.includes('select')); }, mapper: (node) => ({ component: 'Dropdown', props: { label: node.text || 'Dropdown', items: extractDropdownItems(node) }, htmlTag: 'div', className: 'ncua-dropdown' }) }, // EmptyState 매핑 { condition: (node) => { const name = node.name.toLowerCase(); return node.type === 'FRAME' && (name.includes('empty') || name.includes('no-data') || name.includes('no-result')); }, mapper: (node) => ({ component: 'EmptyState', props: { title: findChildText(node, 'title') || 'No Data', description: findChildText(node, 'description') || node.text }, htmlTag: 'div', className: 'ncua-empty-state' }) }, // FeaturedIcon 매핑 { condition: (node) => { const name = node.name.toLowerCase(); return (node.type === 'FRAME' || node.type === 'COMPONENT') && (name.includes('icon') || name.includes('featured')); }, mapper: (node) => ({ component: 'FeaturedIcon', props: { name: 'star', size: inferSize(node), color: inferIconColor(node), theme: 'light' }, htmlTag: 'div', className: 'ncua-featured-icon' }) } ]; // Helper functions function inferButtonTheme(node: SimplifiedNode): string { const name = node.name.toLowerCase(); if (name.includes('primary')) return 'primary'; if (name.includes('secondary')) return 'secondary'; if (name.includes('destructive') || name.includes('danger') || name.includes('delete')) return 'destructive'; if (name.includes('link')) return 'link'; if (name.includes('tertiary')) return 'tertiary'; if (name.includes('gray')) return 'secondary-gray'; return 'primary'; } function inferSize(node: SimplifiedNode): string { if (!node.layout) return 'md'; // 레이아웃 ID로부터 실제 크기 정보를 추정 // 실제 구현에서는 globalVars를 통해 정확한 크기를 가져올 수 있습니다 const name = node.name.toLowerCase(); if (name.includes('xxs') || name.includes('tiny')) return 'xxs'; if (name.includes('xs') || name.includes('small')) return 'xs'; if (name.includes('sm')) return 'sm'; if (name.includes('lg') || name.includes('large')) return 'lg'; if (name.includes('xl') || name.includes('xlarge')) return 'xl'; if (name.includes('2xl') || name.includes('xxlarge')) return '2xl'; return 'md'; } function inferBadgeColor(node: SimplifiedNode): string { const name = node.name.toLowerCase(); if (name.includes('success') || name.includes('green')) return 'success'; if (name.includes('warning') || name.includes('yellow')) return 'warning'; if (name.includes('error') || name.includes('red')) return 'error'; if (name.includes('info') || name.includes('blue')) return 'info'; return 'default'; } function inferNotificationType(node: SimplifiedNode): string { const name = node.name.toLowerCase(); if (name.includes('success')) return 'success'; if (name.includes('warning')) return 'warning'; if (name.includes('error') || name.includes('danger')) return 'error'; if (name.includes('info')) return 'info'; return 'info'; } function findChildText(node: SimplifiedNode, type: string): string | undefined { if (!node.children) return undefined; const child = node.children.find(child => child.name.toLowerCase().includes(type) && child.text ); return child?.text; } function extractTabItems(node: SimplifiedNode): Array<{ label: string; isActive?: boolean }> { if (!node.children) return []; return node.children .filter(child => child.type === 'TEXT' || child.text) .map(child => ({ label: child.text || child.name, isActive: child.name.toLowerCase().includes('active') })); } function extractDropdownItems(node: SimplifiedNode): Array<{ label: string; value: string }> { if (!node.children) return [{ label: '옵션 1', value: '1' }, { label: '옵션 2', value: '2' }]; return node.children .filter(child => child.text) .map((child, index) => ({ label: child.text || `옵션 ${index + 1}`, value: `${index + 1}` })); } function inferTagColor(node: SimplifiedNode): string { const name = node.name.toLowerCase(); if (name.includes('success') || name.includes('green')) return 'success'; if (name.includes('warning') || name.includes('yellow')) return 'warning'; if (name.includes('error') || name.includes('red')) return 'error'; if (name.includes('info') || name.includes('blue')) return 'info'; if (name.includes('primary')) return 'primary'; return 'gray'; } function inferIconColor(node: SimplifiedNode): string { const name = node.name.toLowerCase(); if (name.includes('primary') || name.includes('red')) return 'primary'; if (name.includes('success') || name.includes('green')) return 'success'; if (name.includes('warning') || name.includes('yellow')) return 'warning'; if (name.includes('error') || name.includes('danger')) return 'error'; if (name.includes('info') || name.includes('blue')) return 'info'; return 'gray'; } // 메인 매핑 함수 export function mapToNcdsComponent(node: SimplifiedNode): NcdsComponentMapping | null { for (const rule of NCDS_COMPONENT_RULES) { if (rule.condition(node)) { return rule.mapper(node); } } return null; } // 컴포넌트 매핑 결과를 검증하는 함수 export function validateNcdsMapping(mapping: NcdsComponentMapping): boolean { const validComponents = [ 'Button', 'InputBase', 'Checkbox', 'Radio', 'Select', 'Badge', 'Modal', 'HorizontalTab', 'VerticalTab', 'Pagination', 'ProgressBar', 'ProgressCircle', 'Notification', 'Spinner', 'Tag', 'Tooltip', 'Slider', 'Toggle', 'BreadCrumb', 'Divider', 'Dropdown', 'EmptyState', 'FeaturedIcon' ]; return validComponents.includes(mapping.component); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/nhn-commerce-fe/figma-mcp-ncds'

If you have feedback or need assistance with the MCP directory API, please join our Discord server