#!/usr/bin/env node /** * Add the PC components identified from the Element UI sidebar. * * This generator is intentionally limited to the 24 slugs in DEFS. It refuses * existing slugs or source paths, writes through a temporary file, and keeps * generated five-end sources readable so the normal build/test chain consumes * ordinary repository files rather than a runtime registry. */ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const LIB = path.join(ROOT, '.design_library', 'kole-ui'); const COMPONENTS = path.join(LIB, 'components'); const FRAMEWORKS = path.join(ROOT, 'frameworks'); const ROOT_SPEC = path.join(ROOT, '组件11.txt'); const LIB_SPEC = path.join(LIB, 'specs', '组件11.txt'); const p = (name, type = 'String', def = "''") => ({ name, type, def }); const DEFS = [ { slug: 'layout', prefix: 'Layout', name: '布局 Layout', category: 'general', kind: 'layout', usage: '搭建后台页面的侧边栏、页头、内容区与页脚,并支持折叠和响应式排列。', props: [p('direction', 'String', "'row'"), p('collapsed', 'Boolean', 'false'), p('gap', 'String', "'16px'")], events: [] }, { slug: 'container', prefix: 'Container', name: '布局容器 Container', category: 'general', kind: 'container', usage: '组合页面级 Header、Sider、Main 与 Footer,统一内容宽度和间距。', props: [p('direction', 'String', "'column'"), p('width', 'String', "'100%'") , p('gap', 'String', "'16px'")], events: [] }, { slug: 'typography', prefix: 'Typography', name: '排版 Typography', category: 'general', kind: 'typography', usage: '统一标题、正文、辅助文字、数字和省略文本的层级与阅读体验。', props: [p('variant', 'String', "'body'"), p('text', 'String', "'示例文本'"), p('align', 'String', "'left'"), p('maxLength', 'Number', '0')], events: ['copy'] }, { slug: 'icon', prefix: 'Icon', name: '图标 Icon', category: 'general', kind: 'icon', usage: '为菜单、状态、操作和提示提供一致的图形语义,可使用文本或 SVG 图标名。', props: [p('icon', 'String', "'★'"), p('size', 'String', "'20px'"), p('color', 'String', "'brand'")], events: ['click'] }, { slug: 'link', prefix: 'Link', name: '文字链接 Link', category: 'general', kind: 'link', usage: '跳转详情、下载文件或打开外部地址,提供主次和禁用状态。', props: [p('href', 'String', "'#'"), p('target', 'String', "'_self'"), p('variant', 'String', "'primary'"), p('disabled', 'Boolean', 'false')], events: ['click'] }, { slug: 'radio', prefix: 'Radio', name: '单选框 Radio', category: 'input', kind: 'radio', usage: '在互斥选项中选择一个结果,例如审批结论、显示模式或筛选维度。', props: [p('modelValue', 'String', "'a'"), p('options', 'Array', '() => []'), p('disabled', 'Boolean', 'false')], events: ['update:modelValue', 'change'] }, { slug: 'checkbox', prefix: 'Checkbox', name: '复选框 Checkbox', category: 'input', kind: 'checkbox', usage: '选择多个权限、筛选条件或数据记录,支持全选和半选状态。', props: [p('modelValue', 'Array', '() => []'), p('options', 'Array', '() => []'), p('disabled', 'Boolean', 'false')], events: ['update:modelValue', 'change'] }, { slug: 'inputnumber', prefix: 'InputNumber', name: '计数器 InputNumber', category: 'input', kind: 'inputnumber', usage: '输入数量、金额或排序权重,支持最小值、最大值、步长和精度。', props: [p('value', 'Number', '1'), p('min', 'Number', '0'), p('max', 'Number', '100'), p('step', 'Number', '1'), p('precision', 'Number', '0'), p('disabled', 'Boolean', 'false')], events: ['change'] }, { slug: 'switch', prefix: 'Switch', name: '开关 Switch', category: 'input', kind: 'switch', usage: '即时启停配置项或业务状态,例如启用通知、公开数据和自动同步。', props: [p('checked', 'Boolean', 'true'), p('disabled', 'Boolean', 'false'), p('size', 'String', "'default'")], events: ['change', 'update:modelValue'] }, { slug: 'timepicker', prefix: 'TimePicker', name: '时间选择器 TimePicker', category: 'input', kind: 'timepicker', usage: '设置营业时段、任务执行时间或提醒时间,采用无运行时依赖的原生时间控件。', props: [p('value', 'String', "'09:30'"), p('format', 'String', "'HH:mm'")], events: ['change'] }, { slug: 'datepicker', prefix: 'DatePicker', name: '日期选择器 DatePicker', category: 'input', kind: 'datepicker', usage: '选择单日或起止日期,适用于查询条件、有效期和排期设置。', props: [p('value', 'String', "'2026-09-20'"), p('range', 'Boolean', 'false'), p('format', 'String', "'YYYY-MM-DD'"), p('presets', 'Array', '() => []')], events: ['change'] }, { slug: 'datetimepicker', prefix: 'DateTimePicker', name: '日期时间选择器 DateTimePicker', category: 'input', kind: 'datetimepicker', usage: '设置精确到时分的预约、生效或发布时刻。', props: [p('value', 'String', "'2026-09-20T09:30'"), p('format', 'String', "'YYYY-MM-DD HH:mm'")], events: ['change'] }, { slug: 'form', prefix: 'Form', name: '表单 Form', category: 'input', kind: 'form', usage: '统一字段布局、必填标识、校验错误和提交/重置流程。', props: [p('fields', 'Array', '() => []'), p('label', 'String', "'名称'"), p('required', 'Boolean', 'true'), p('error', 'String', "''")], events: ['submit', 'reset'] }, { slug: 'pagination', prefix: 'Pagination', name: '分页 Pagination', category: 'display', kind: 'pagination', usage: '在表格或卡片列表底部浏览大数据集,支持页码、总数、每页条数和跳转。', props: [p('total', 'Number', '128'), p('page', 'Number', '1'), p('pageSize', 'Number', '10'), p('size', 'String', "'default'")], events: ['pagechange', 'sizechange'] }, { slug: 'badge', prefix: 'Badge', name: '标记 Badge', category: 'display', kind: 'badge', usage: '显示未读数、待办数、小红点或紧贴对象的状态标记。', props: [p('count', 'Number', '8'), p('status', 'String', "'error'"), p('variant', 'String', "'dot'")], events: [] }, { slug: 'avatar', prefix: 'Avatar', name: '头像 Avatar', category: 'display', kind: 'avatar', usage: '展示成员、审批人或操作者,支持文字、图片、尺寸和在线状态点。', props: [p('src', 'String', "''"), p('avatar', 'String', "'林'"), p('size', 'String', "'40px'"), p('status', 'String', "'online'")], events: ['click'] }, { slug: 'descriptions', prefix: 'Descriptions', name: '描述列表 Descriptions', category: 'display', kind: 'descriptions', usage: '以只读方式展示订单、客户、员工等对象的结构化详情,支持 1–4 列。', props: [p('fields', 'Array', '() => []'), p('columns', 'Number', '3'), p('bordered', 'Boolean', 'true')], events: [] }, { slug: 'alert', prefix: 'Alert', name: '警告 Alert', category: 'feedback', kind: 'alert', usage: '在页面内持续展示风险、限制、成功结果或操作说明。', props: [p('type', 'String', "'info'"), p('title', 'String', "'系统提示'"), p('description', 'String', "'请检查当前配置后继续。'"), p('closable', 'Boolean', 'true')], events: ['close'] }, { slug: 'pageheader', prefix: 'PageHeader', name: '页头 PageHeader', category: 'navigation', kind: 'pageheader', usage: '在详情页提供返回、标题、副标题和页面级操作。', props: [p('title', 'String', "'用户详情'"), p('description', 'String', "'查看并编辑用户资料'"), p('secondaryText', 'String', "'返回列表'")], events: ['back'] }, { slug: 'tooltip', prefix: 'Tooltip', name: '文字提示 Tooltip', category: 'feedback', kind: 'tooltip', usage: '解释图标、截断内容或术语,适合短文本的 hover、focus 和键盘提示。', props: [p('tooltip', 'String', "'这是补充说明'"), p('placement', 'String', "'top'"), p('trigger', 'String', "'hover'")], events: ['show', 'hide'] }, { slug: 'popover', prefix: 'Popover', name: '弹出框 Popover', category: 'feedback', kind: 'popover', usage: '展示比 Tooltip 更丰富的就近信息、表单或操作,支持点击和 Escape 关闭。', props: [p('content', 'String', "'这里可以放解释、统计或操作。'"), p('placement', 'String', "'bottom'"), p('trigger', 'String', "'click'"), p('open', 'Boolean', 'false')], events: ['open', 'close'] }, { slug: 'divider', prefix: 'Divider', name: '分割线 Divider', category: 'display', kind: 'divider', usage: '分隔表单段落、菜单组或内容块,支持水平/垂直方向和带文字分隔。', props: [p('orientation', 'String', "'horizontal'"), p('title', 'String', "'更多信息'"), p('variant', 'String', "'solid'")], events: [] }, { slug: 'infinitescroll', prefix: 'InfiniteScroll', name: '无限滚动 InfiniteScroll', category: 'display', kind: 'infinitescroll', usage: '逐批加载动态流或长列表,明确展示加载中、完成和可重试状态。', props: [p('items', 'Array', '() => []'), p('loading', 'Boolean', 'false'), p('status', 'String', "'more'"), p('height', 'String', "'180px'")], events: ['load'] }, { slug: 'drawer', prefix: 'Drawer', name: '抽屉 Drawer', category: 'feedback', kind: 'drawer', usage: '不离开列表页,侧向查看或编辑详情,默认从右侧滑出并带遮罩。', props: [p('open', 'Boolean', 'false'), p('title', 'String', "'编辑资料'"), p('placement', 'String', "'right'"), p('maskClosable', 'Boolean', 'true')], events: ['open', 'close', 'confirm'] } ]; const bySlug = new Map(DEFS.map((d) => [d.slug, d])); const esc = (s) => String(s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); const jsQuote = (s) => JSON.stringify(String(s)); function atomicWrite(file, content) { fs.mkdirSync(path.dirname(file), { recursive: true }); const tmp = `${file}.tmp-${process.pid}`; fs.writeFileSync(tmp, content.replace(/\r\n/g, '\n'), 'utf8'); fs.renameSync(tmp, file); } function assertFree(idx) { const existing = new Set(idx.components.map((c) => c.slug)); for (const d of DEFS) { const registered = existing.has(d.slug); const allSources = ['.css', '.html', '.jsx', '.vue2.vue', '.vue3.vue'].map((ext) => path.join(FRAMEWORKS, d.prefix + ext)); const sourceCount = allSources.filter((file) => fs.existsSync(file)).length; if (registered && sourceCount !== 5) throw new Error(`refusing partial component: ${d.slug} has ${sourceCount}/5 source files`); if (!registered && sourceCount) throw new Error(`refusing partial source set: ${d.slug} has ${sourceCount}/5 source files`); } } function contract(d) { const dims = d.props.filter((x) => ['variant', 'size', 'type', 'placement', 'orientation', 'status', 'range', 'trigger'].includes(x.name)); return { schemaVersion: 2, sourceKind: 'structured-spec', confidence: 'high', slug: d.slug, name: d.name, semanticTypeCandidates: [d.kind, d.kind === 'form' ? 'form' : 'widget'], variantDimensions: dims.map((x) => ({ name: x.name, values: variantValues(d, x.name) })), representativeVariants: variantValues(d, 'variant').map((v) => ({ variant: v, label: `${d.name.split(' ')[0]} · ${v}` })), anatomy: anatomy(d), structurePatterns: { root: `kole-${d.slug}`, states: 'default / hover / focus / disabled(按组件语义展示)', responsive: '使用 CSS 令牌与 flex/grid,宿主可覆盖容器宽度' }, usageHints: [d.usage, '五端保持相同的类名、状态和基础语义。', '交互控件优先使用原生元素,受控值变化通过 change 或 v-model 事件传递。'], doNotInvent: ['不把本组件扩展为外部依赖提供的完整业务流程。', '未声明的高级主题、数据请求和服务端校验不由组件自行推断。'], unknowns: ['宿主项目的国际化文案、数据请求和权限策略由接入方决定。'] }; } function variantValues(d, name) { const common = { variant: ['default', 'primary', 'compact'], size: ['small', 'default', 'large'], type: ['info', 'success', 'warning', 'error'], placement: ['top', 'right', 'bottom'], orientation: ['horizontal', 'vertical'], status: ['more', 'loading', 'done'], range: ['single', 'range'], trigger: ['hover', 'click'] }; if (common[name]) return common[name]; if (d.kind === 'layout' || d.kind === 'container') return ['row', 'column']; return ['default', 'active', 'disabled']; } function anatomy(d) { const base = { root: `kole-${d.slug}`, content: `${d.name.split(' ')[0]} 主体内容`, state: '状态类 is-* 与 ARIA 属性同步' }; if (['tooltip', 'popover', 'drawer', 'alert'].includes(d.kind)) base.overlay = '提示/面板/状态容器与关闭控件'; if (['radio', 'checkbox', 'switch', 'inputnumber', 'timepicker', 'datepicker', 'datetimepicker', 'form'].includes(d.kind)) base.control = '原生表单控件或可聚焦操作控件'; return base; } function cssFor(d) { const s = `kole-${d.slug}`; return `/* Kole UI ${d.name} · generated from 组件11.txt */ .${s} { box-sizing: border-box; font-family: var(--kole-font-family); color: var(--kole-color-text-body); } .${s} *, .${s} *::before, .${s} *::after { box-sizing: border-box; } .${s}-surface, .${s}-panel { background: var(--kole-color-card-bg); border: 1px solid var(--kole-color-border); border-radius: var(--kole-radius-medium); box-shadow: var(--kole-shadow-low); } .${s}-title { color: var(--kole-color-text-title); font-weight: var(--kole-type-h2-weight); } .${s}-desc { color: var(--kole-color-text-secondary); font-size: var(--kole-type-caption-size); line-height: var(--kole-type-caption-height); } .${s} button, .${s} input, .${s} select, .${s} a { font: inherit; } .${s} button, .${s} a { cursor: pointer; } .${s} button:focus-visible, .${s} a:focus-visible, .${s} input:focus-visible, .${s} select:focus-visible, .${s}-control:focus-visible { outline: 2px solid var(--kole-color-brand); outline-offset: 2px; } .${s}-primary { color: var(--kole-color-text-inverse); background: var(--kole-color-brand); border-color: var(--kole-color-brand); } .${s}-secondary { color: var(--kole-color-brand); background: var(--kole-color-brand-bg); border-color: var(--kole-color-brand); } .${s}-compact { font-size: var(--kole-type-caption-size); padding: var(--kole-space-4) var(--kole-space-8); } .${s}-disabled, .${s} [disabled] { cursor: not-allowed; opacity: .55; } .${s}-row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--kole-space-8); } .${s}-button { min-height: var(--kole-control-height-default); padding: 0 var(--kole-space-12); border: 1px solid var(--kole-color-border); border-radius: var(--kole-radius-base); background: var(--kole-color-card-bg); color: var(--kole-color-text-body); } .${s}-control { min-height: var(--kole-control-height-default); border: 1px solid var(--kole-color-border); border-radius: var(--kole-radius-base); background: var(--kole-color-card-bg); color: var(--kole-color-text-body); padding: 0 var(--kole-space-8); } .${s}-item { display: inline-flex; align-items: center; gap: var(--kole-space-8); min-height: var(--kole-control-height-default); } .${s}-panel { padding: var(--kole-space-16); min-width: 220px; } .${s}-layout { display: grid; grid-template-columns: 180px 1fr; min-height: 180px; gap: var(--kole-space-16); } .${s}-layout.is-collapsed { grid-template-columns: 64px 1fr; } .${s}-header, .${s}-footer { padding: var(--kole-space-12) var(--kole-space-16); background: var(--kole-color-table-header-bg); } .${s}-main { padding: var(--kole-space-16); } .${s}-vertical { flex-direction: column; align-items: stretch; } .${s}-horizontal { flex-direction: row; } .${s}-input-row { display: flex; align-items: center; gap: var(--kole-space-4); } .${s}-number { width: 96px; text-align: center; } .${s}-switch { display: inline-flex; width: 44px; height: 22px; padding: 2px; border-radius: 999px; background: var(--kole-color-border-strong); align-items: center; transition: background var(--kole-duration-fast); } .${s}-switch::after { content: ''; width: 18px; height: 18px; border-radius: 50%; background: var(--kole-color-card-bg); box-shadow: var(--kole-shadow-low); transition: transform var(--kole-duration-fast); } .${s}-switch.is-on { background: var(--kole-color-brand); } .${s}-switch.is-on::after { transform: translateX(22px); } .${s}-option { display: inline-flex; align-items: center; gap: var(--kole-space-4); } .${s}-form { display: grid; gap: var(--kole-space-12); max-width: 420px; } .${s}-field { display: grid; gap: var(--kole-space-4); } .${s}-error { color: var(--kole-color-error); font-size: var(--kole-type-caption-size); } .${s}-pager { display: flex; align-items: center; flex-wrap: wrap; gap: var(--kole-space-4); } .${s}-page, .${s}-action { min-width: 32px; min-height: 32px; border: 1px solid var(--kole-color-border); background: var(--kole-color-card-bg); border-radius: var(--kole-radius-base); } .${s}-page.is-current { color: var(--kole-color-text-inverse); background: var(--kole-color-brand); border-color: var(--kole-color-brand); } .${s}-badge-host { display: inline-flex; position: relative; padding: var(--kole-space-12); } .${s}-badge { position: absolute; top: 0; right: 0; transform: translate(35%, -35%); min-width: 18px; height: 18px; padding: 0 5px; border-radius: 9px; color: var(--kole-color-text-inverse); background: var(--kole-color-error); font-size: 11px; line-height: 18px; text-align: center; } .${s}-badge.is-success { background: var(--kole-color-success); } .${s}-badge.is-info { background: var(--kole-color-info); } .${s}-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--kole-color-error); } .${s}-avatar { display: inline-flex; width: 40px; height: 40px; align-items: center; justify-content: center; overflow: hidden; border-radius: 50%; color: var(--kole-color-text-inverse); background: var(--kole-color-brand); font-weight: 600; } .${s}-avatar img { width: 100%; height: 100%; object-fit: cover; } .${s}-avatar.is-offline { filter: grayscale(1); opacity: .65; } .${s}-status { width: 9px; height: 9px; margin-left: -10px; margin-top: 28px; border: 2px solid var(--kole-color-card-bg); border-radius: 50%; background: var(--kole-color-success); } .${s}-table { width: 100%; border-collapse: collapse; } .${s}-table th, .${s}-table td { padding: var(--kole-space-8) var(--kole-space-12); border-bottom: 1px solid var(--kole-color-border); text-align: start; } .${s}-table th { background: var(--kole-color-table-header-bg); color: var(--kole-color-text-secondary); } .${s}-alert { display: flex; align-items: flex-start; gap: var(--kole-space-8); padding: var(--kole-space-12) var(--kole-space-16); border: 1px solid var(--kole-color-border); border-radius: var(--kole-radius-base); background: var(--kole-color-brand-bg); } .${s}-alert.is-success { background: var(--kole-color-tag-green-bg); } .${s}-alert.is-warning { background: color-mix(in srgb, var(--kole-color-warning) 10%, var(--kole-color-card-bg)); } .${s}-alert.is-error { background: color-mix(in srgb, var(--kole-color-error) 10%, var(--kole-color-card-bg)); } .${s}-close { margin-inline-start: auto; border: 0; background: transparent; color: var(--kole-color-text-secondary); } .${s}-pageheader { display: flex; align-items: center; gap: var(--kole-space-12); padding: var(--kole-space-12) var(--kole-space-16); border-bottom: 1px solid var(--kole-color-border); } .${s}-pageheader-main { flex: 1; } .${s}-link { color: var(--kole-color-brand); text-decoration: none; } .${s}-link.is-disabled { pointer-events: none; color: var(--kole-color-text-disabled); } .${s}-tooltip-wrap, .${s}-popover-wrap { position: relative; display: inline-flex; } .${s}-tip, .${s}-popover { position: absolute; z-index: 10; top: calc(100% + var(--kole-space-8)); left: 0; min-width: 160px; max-width: 260px; padding: var(--kole-space-8) var(--kole-space-12); border-radius: var(--kole-radius-base); box-shadow: var(--kole-shadow-medium); } .${s}-tip { color: var(--kole-color-text-inverse); background: var(--kole-color-tooltip-bg); } .${s}-popover { color: var(--kole-color-text-body); background: var(--kole-color-card-bg); border: 1px solid var(--kole-color-border); } .${s}-tip[hidden], .${s}-popover[hidden], .${s}-drawer[hidden] { display: none; } .${s}-divider { border: 0; border-top: 1px solid var(--kole-color-border); margin: var(--kole-space-16) 0; } .${s}-divider.is-vertical { width: 1px; height: 32px; margin: 0 var(--kole-space-8); border-top: 0; border-left: 1px solid var(--kole-color-border); } .${s}-scroll { height: 180px; overflow: auto; padding: var(--kole-space-8); border: 1px solid var(--kole-color-border); border-radius: var(--kole-radius-base); } .${s}-scroll-item { padding: var(--kole-space-8); border-bottom: 1px solid var(--kole-color-border); } .${s}-drawer-mask { position: fixed; inset: 0; z-index: 20; background: var(--kole-color-mask); } .${s}-drawer { position: fixed; z-index: 21; top: 0; right: 0; width: 360px; max-width: 90vw; height: 100vh; padding: var(--kole-space-16); background: var(--kole-color-card-bg); box-shadow: var(--kole-shadow-high); } .${s}-drawer-footer { position: absolute; right: var(--kole-space-16); bottom: var(--kole-space-16); left: var(--kole-space-16); display: flex; justify-content: flex-end; gap: var(--kole-space-8); } @media (prefers-reduced-motion: reduce) { .${s} *, .${s} *::before, .${s} *::after { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; } } `; } function demoMarkup(d) { const s = `kole-${d.slug}`; const common = { layout: `
辅助说明文字,展示信息层级和阅读节奏。
长文本省略示例:订单提交后将进入审核流程……
| 订单号 | KOLE-20260920 | 状态 | 处理中 |
|---|---|---|---|
| 负责人 | 林工 | 更新时间 | 今天 09:30 |
${esc(d.usage)}
${esc(d.usage)}
零运行时依赖;数据请求、权限和国际化由宿主接入方决定。
辅助说明文字,展示信息层级和省略。