Files
aurora-admin/tools/add-element-components.mjs
aurora-admin f1fbfc2ddb
Regression / regression (push) Canceled after 0s
feat(品牌标识): 几何 K 图标(favicon/顶栏标记/theme-color) + 并行会话成果入库
## 品牌标识(本次会话)

起因:品牌此前没有任何图形标识 —— 唯一 favicon 是内联 data-URI 里的字母「A」,
那是 v2.0.0「Aurora Admin → Kole UI」改名漏掉的一处(PC 顶栏也是「A」,
移动端站已是「K」;移动端文档站则完全没有 favicon)。

- 几何:24 网格三个互不接触的笔画(竖 + 两斜),圆头描边;
  描边 2.25 → 16px 标签页尺寸下正好 1.5px = 规范原文「描边1.5px」
- 取色分两套(刻意):favicon 硬编码品牌蓝/白(渲染在浏览器标签栏,不继承 kole-dark);
  顶栏标记走 currentColor(实测暗色下自动转 rgb(20,22,28))
- 新增 theme-color 双条(light #FFFFFF / dark #1C1F26,取 --kole-color-card-bg)
- 修 site/app.js hero 标语 KOLE ADMIN → KOLE UI(改名变形残留)
- 移动端 7 个模板补 favicon(此前计数 0)

验收:门禁 9 条全 OK(site-routing/site-routes/mobile-docs/mobile-site/isolation/
theme/nav/i18n/icons);PC 回归 1464/1464 · 移动端 807/807,各连跑 8 次一致;
两端 favicon 405 字节逐字节一致;PC 站控制台错误 1→0。

## 并行会话成果(本次一并入库)

- 图标系统:2576 图标(TDesign/Element Plus,MIT)+ 11 端注入 + 5 个构建门禁工具
  + IconPreview 预览页 + ICON-SPEC.md 冻结规格
- 移动端平台:47 组件 × 6 端 + 文档站 53 页 + 隔离门禁
- PC 组件:103 个大后台组件 / 组件11 批次
- uni-app:PC 端试点 + 移动端端实现 + 真实编译验证

## 工程

- .gitignore 补 .scratch/ 与 .zcode-preexisting-*.txt(会话中间产物,实测 9.1MB,不入库)
- CHANGELOG 补品牌标识条目
- ROADMAP 登 S8-P4(品牌标识任务包 + og:image/apple-touch-icon 未做部分)
2026-09-21 10:05:48 +08:00

397 lines
58 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
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: `<div class="${s}-layout"><aside class="${s}-surface">导航<br>用户<br>设置</aside><section class="${s}-surface"><div class="${s}-header">页头</div><div class="${s}-main">内容区</div><div class="${s}-footer">页脚</div></section></div>`,
container: `<div class="${s} ${s}-surface" style="max-width:720px"><div class="${s}-header">Header</div><div class="${s}-main">Main content</div><div class="${s}-footer">Footer</div></div>`,
typography: `<div><h2 class="${s}-title">页面标题</h2><p class="${s}-desc">辅助说明文字,展示信息层级和阅读节奏。</p><p class="${s}-compact">长文本省略示例:订单提交后将进入审核流程……</p></div>`,
icon: `<button class="${s}-button ${s}-primary" type="button" aria-label="收藏"><span class="${s}-icon" aria-hidden="true">★</span> 收藏</button><span class="${s}-icon" role="img" aria-label="成功">✓</span>`,
link: `<a class="${s}-link" href="#detail">查看详情</a><a class="${s}-link ${s}-secondary" href="#download">下载文件</a>`,
radio: `<label class="${s}-option"><input type="radio" name="${s}-demo" value="a" checked> 选项 A</label><label class="${s}-option"><input type="radio" name="${s}-demo" value="b"> 选项 B</label>`,
checkbox: `<label class="${s}-option"><input type="checkbox" checked> 邮件通知</label><label class="${s}-option"><input type="checkbox"> 短信通知</label>`,
inputnumber: `<div class="${s}-input-row"><button class="${s}-action" type="button" data-minus aria-label="减少">−</button><input class="${s}-control ${s}-number" type="number" value="3" min="0" max="99" aria-label="数量"><button class="${s}-action" type="button" data-plus aria-label="增加">+</button></div>`,
switch: `<button class="${s}-switch is-on" type="button" role="switch" aria-checked="true" aria-label="启用通知" data-toggle></button><span>启用通知</span>`,
timepicker: `<label class="${s}-item">执行时间 <input class="${s}-control" type="time" value="09:30" aria-label="执行时间"></label>`,
datepicker: `<label class="${s}-item">日期 <input class="${s}-control" type="date" value="2026-09-20" aria-label="日期"></label><button class="${s}-button ${s}-secondary" type="button">本月</button>`,
datetimepicker: `<label class="${s}-item">生效时间 <input class="${s}-control" type="datetime-local" value="2026-09-20T09:30" aria-label="生效时间"></label>`,
form: `<form class="${s}-form" id="${s}-form"><label class="${s}-field"><span>名称 <b aria-hidden="true">*</b></span><input class="${s}-control" name="name" required value="示例项目"></label><span class="${s}-error" hidden>请输入名称</span><div class="${s}-row"><button class="${s}-button ${s}-primary" type="submit">提交</button><button class="${s}-button" type="reset">重置</button></div></form>`,
pagination: `<div class="${s}-pager" aria-label="分页"><button class="${s}-page" type="button" data-prev>上一页</button><button class="${s}-page is-current" type="button">1</button><button class="${s}-page" type="button">2</button><button class="${s}-page" type="button" data-next>下一页</button><span class="${s}-desc">共 128 条</span></div>`,
badge: `<span class="${s}-badge-host"><span class="${s}-surface" style="padding:12px">消息</span><span class="${s}-badge" aria-label="8 条未读">8</span></span><span class="${s}-badge-host"><span class="${s}-surface" style="padding:12px">状态</span><span class="${s}-dot" aria-label="异常"></span></span>`,
avatar: `<span class="${s}-item"><span class="${s}-avatar" aria-label="林工">林</span><span class="${s}-status" aria-label="在线"></span></span><span class="${s}-avatar ${s}-avatar is-offline" aria-label="陈工">陈</span>`,
descriptions: `<table class="${s}-table"><tbody><tr><th>订单号</th><td>KOLE-20260920</td><th>状态</th><td>处理中</td></tr><tr><th>负责人</th><td>林工</td><th>更新时间</th><td>今天 09:30</td></tr></tbody></table>`,
alert: `<div class="${s}-alert" role="alert"><span><strong>系统提示</strong><br><span class="${s}-desc">请检查当前配置后继续。</span></span><button class="${s}-close" type="button" aria-label="关闭" data-close>×</button></div>`,
pageheader: `<header class="${s}-pageheader"><button class="${s}-button" type="button" data-back>← 返回</button><div class="${s}-pageheader-main"><strong class="${s}-title">用户详情</strong><div class="${s}-desc">查看并编辑用户资料</div></div><button class="${s}-button ${s}-primary" type="button">保存</button></header>`,
tooltip: `<div class="${s}-tooltip-wrap"><button class="${s}-button ${s}-primary" type="button" aria-describedby="${s}-tip" data-tip>悬停或聚焦</button><span class="${s}-tip" id="${s}-tip" role="tooltip" hidden>这是补充说明</span></div>`,
popover: `<div class="${s}-popover-wrap"><button class="${s}-button ${s}-primary" type="button" aria-expanded="false" aria-controls="${s}-pop" data-pop>查看信息</button><div class="${s}-popover" id="${s}-pop" role="dialog" hidden>这里可以放解释、统计或操作。</div></div>`,
divider: `<div style="width:100%"><div class="${s}-desc">基础信息</div><hr class="${s}-divider"><div class="${s}-desc">安全设置</div></div><div class="${s}-divider is-vertical" role="separator" aria-label="分隔"></div>`,
infinitescroll: `<div class="${s}-scroll" role="feed" aria-label="动态列表"><div class="${s}-scroll-item">活动记录 1</div><div class="${s}-scroll-item">活动记录 2</div><button class="${s}-button" type="button" data-load>加载更多</button></div>`,
drawer: `<button class="${s}-button ${s}-primary" type="button" data-open>打开抽屉</button><div class="${s}-drawer-mask" hidden data-mask></div><aside class="${s}-drawer" hidden role="dialog" aria-modal="true" aria-labelledby="${s}-title"><h3 id="${s}-title" class="${s}-title">编辑资料</h3><p class="${s}-desc">在当前页面侧向编辑,不丢失列表上下文。</p><div class="${s}-drawer-footer"><button class="${s}-button" type="button" data-close>取消</button><button class="${s}-button ${s}-primary" type="button" data-confirm>确定</button></div></aside>`
};
return common[d.kind] || `<div class="${s}-surface"><strong class="${s}-title">${esc(d.name)}</strong><p class="${s}-desc">${esc(d.usage)}</p></div>`;
}
function demoScript(d) {
const s = `kole-${d.slug}`;
const lines = [];
if (['switch'].includes(d.kind)) lines.push(`document.querySelector('.${s} [data-toggle]').addEventListener('click', function () { var on = this.getAttribute('aria-checked') !== 'true'; this.setAttribute('aria-checked', String(on)); this.classList.toggle('is-on', on); });`);
if (d.kind === 'inputnumber') lines.push(`var n = document.querySelector('.${s}-number'); document.querySelector('.${s} [data-minus]').addEventListener('click', function () { n.stepDown(); }); document.querySelector('.${s} [data-plus]').addEventListener('click', function () { n.stepUp(); });`);
if (d.kind === 'form') lines.push(`document.getElementById('${s}-form').addEventListener('submit', function (e) { e.preventDefault(); this.querySelector('.${s}-error').hidden = this.elements.name.value.trim() !== ''; });`);
if (d.kind === 'alert') lines.push(`document.querySelector('.${s} [data-close]').addEventListener('click', function () { this.closest('.${s}-alert').hidden = true; });`);
if (d.kind === 'pageheader') lines.push(`document.querySelector('.${s} [data-back]').addEventListener('click', function () { this.setAttribute('aria-label', '已返回'); });`);
if (d.kind === 'tooltip') lines.push(`var tip = document.getElementById('${s}-tip'); var trigger = document.querySelector('.${s} [data-tip]'); trigger.addEventListener('mouseenter', function () { tip.hidden = false; }); trigger.addEventListener('mouseleave', function () { tip.hidden = true; }); trigger.addEventListener('focus', function () { tip.hidden = false; }); trigger.addEventListener('blur', function () { tip.hidden = true; });`);
if (d.kind === 'popover') lines.push(`var pop = document.getElementById('${s}-pop'); document.querySelector('.${s} [data-pop]').addEventListener('click', function () { pop.hidden = !pop.hidden; this.setAttribute('aria-expanded', String(!pop.hidden)); });`);
if (d.kind === 'infinitescroll') lines.push(`document.querySelector('.${s} [data-load]').addEventListener('click', function () { var item = document.createElement('div'); item.className = '${s}-scroll-item'; item.textContent = '活动记录 3'; this.before(item); this.textContent = '已加载'; this.disabled = true; });`);
if (d.kind === 'drawer') lines.push(`var drawer = document.querySelector('.${s}-drawer'); var mask = document.querySelector('.${s}-drawer-mask'); function closeDrawer() { drawer.hidden = true; mask.hidden = true; } document.querySelector('.${s} [data-open]').addEventListener('click', function () { drawer.hidden = false; mask.hidden = false; }); document.querySelector('.${s} [data-mask]').addEventListener('click', closeDrawer); document.querySelector('.${s} [data-close]').addEventListener('click', closeDrawer); document.querySelector('.${s} [data-confirm]').addEventListener('click', closeDrawer);`);
return lines.length ? `(function () { ${lines.join(' ')} }());` : '';
}
function htmlFor(d) {
const s = `kole-${d.slug}`;
return `<!DOCTYPE html>
<html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Kole UI · ${esc(d.name)}(普通 H5)</title>
<link rel="stylesheet" href="../.design_library/kole-ui/colors_and_type.css"><link rel="stylesheet" href="${d.prefix}.css">
<style>.demo{padding:24px;background:var(--kole-color-page-bg);font-family:var(--kole-font-family)}.demo h1{margin:0 0 4px;font-size:20px;color:var(--kole-color-text-title)}.sub{margin:0 0 20px;color:var(--kole-color-text-secondary);font-size:12px}.demo-block{padding:16px;margin-bottom:16px;background:var(--kole-color-card-bg);border:1px solid var(--kole-color-border);border-radius:var(--kole-radius-large)}.demo-block h2{margin:0 0 12px;font-size:14px;color:var(--kole-color-text-title)}.row{display:flex;flex-wrap:wrap;align-items:center;gap:12px}.demo-note{margin:12px 0 0;color:var(--kole-color-text-secondary);font-size:12px}</style></head>
<body><div class="demo"><h1>${esc(d.name)}(纯 H5)</h1><p class="sub">${esc(d.usage)}</p><div class="demo-block"><h2>默认与常用变体</h2><div class="row ${s}">${demoMarkup(d)}</div></div><div class="demo-block"><h2>状态与使用场景</h2><div class="row ${s}"><div class="${s}-surface" style="padding:12px">${esc(d.usage)}</div><span class="${s}-desc">支持键盘焦点、状态反馈和令牌化样式。</span></div><p class="demo-note">零运行时依赖;数据请求、权限和国际化由宿主接入方决定。</p></div></div><script>${demoScript(d)}</script></body></html>`;
}
function propObject(d, vue2 = false) {
return d.props.map((x) => {
const def = x.def === '() => []' ? (vue2 ? 'function () { return []; }' : '() => []') : x.def;
return ` ${x.name}: { type: ${x.type}, default: ${def} }, // ${descFor(x.name)}`;
}).join('\n');
}
function descFor(name) {
const map = { type: '类型', variant: '变体', size: '尺寸', state: '状态', status: '状态', mode: '模式', disabled: '是否禁用', loading: '是否加载中', readonly: '是否只读', required: '是否必填', placeholder: '占位提示文本', value: '绑定值', modelValue: '双向绑定值', options: '选项数据', data: '数据源', items: '数据项', multiple: '是否多选', clearable: '是否可清空', maxLength: '最大字符数', icon: '图标', title: '标题', description: '描述文本', content: '内容', align: '对齐方式', width: '宽度', height: '高度', columns: '列配置', total: '总数', page: '当前页码', pageSize: '每页条数', checked: '是否选中', open: '是否展开', visible: '是否可见', label: '标签文本', format: '格式', min: '最小值', max: '最大值', step: '步长', precision: '精度(小数位)', error: '错误提示', duration: '持续时间', closable: '是否可关闭', bordered: '是否显示边框', scrollable: '是否可滚动', placement: '弹出位置', trigger: '触发方式', theme: '主题', color: '颜色', count: '数量', avatar: '头像地址', src: '资源地址', href: '链接地址', target: '打开方式', collapsed: '是否折叠', direction: '方向', orientation: '方向', range: '取值范围', presets: '快捷预设', secondaryText: '次要文本', maskClosable: '点击遮罩是否可关闭' };
return map[name] || name;
}
function reactProps(d) {
const parts = d.props.map((x) => `${x.name} = ${x.def === '() => []' ? '[]' : x.def}`);
parts.push('children', 'onChange', 'onClick', 'onClose', 'onSubmit', 'onLoadMore');
return parts.join(', ');
}
function reactBody(d) {
const s = `kole-${d.slug}`;
const body = {
layout: String.raw`<div className="__S__ __S__-layout"><aside className="__S__-surface">导航<br />用户<br />设置</aside><section className="__S__-surface"><div className="__S__-header">页头</div><div className="__S__-main">{children || '内容区'}</div><div className="__S__-footer">页脚</div></section></div>`,
container: String.raw`<div className="__S__ __S__-surface"><div className="__S__-header">Header</div><div className="__S__-main">{children || 'Main content'}</div><div className="__S__-footer">Footer</div></div>`,
typography: String.raw`<div className="__S__"><h2 className="__S__-title">{text}</h2><p className="__S__-desc">辅助说明文字,展示信息层级和省略。</p><button type="button" className="__S__-button" onClick={() => onClick && onClick(text)}>复制文本</button></div>`,
icon: String.raw`<button type="button" className="__S__ __S__-button __S__-primary" style={{ fontSize: size, color: color === 'brand' ? undefined : color }} onClick={onClick} aria-label={icon}>{icon}</button>`,
link: String.raw`<a className={disabled ? '__S__-link __S__-disabled' : '__S__-link'} href={disabled ? undefined : href} target={target} onClick={onClick}>{children || '查看详情'}</a>`,
radio: String.raw`<div className="__S__" role="radiogroup">{opts.map((o) => <label key={o.value} className="__S__-option"><input type="radio" name="__S__-group" checked={modelValue === o.value} disabled={disabled} onChange={() => onChange && onChange(o.value)} />{o.label}</label>)}</div>`,
checkbox: String.raw`<div className="__S__" role="group">{opts.map((o) => <label key={o.value} className="__S__-option"><input type="checkbox" name="__S__-group" checked={Array.isArray(modelValue) && modelValue.includes(o.value)} disabled={disabled} onChange={() => onChange && onChange(o.value)} />{o.label}</label>)}</div>`,
inputnumber: String.raw`<div className="__S__ __S__-input-row"><button className="__S__-action" type="button" onClick={() => onChange && onChange(Number(value) - step)} aria-label="减少">−</button><input className="__S__-control __S__-number" type="number" value={value} min={min} max={max} step={step} disabled={disabled} onChange={(e) => onChange && onChange(Number(e.target.value))} /><button className="__S__-action" type="button" onClick={() => onChange && onChange(Number(value) + step)} aria-label="增加">+</button></div>`,
switch: String.raw`<button className={checked ? '__S__ __S__-switch is-on' : '__S__ __S__-switch'} type="button" role="switch" aria-checked={checked} disabled={disabled} onClick={() => onChange && onChange(!checked)} aria-label="切换状态" />`,
timepicker: String.raw`<label className="__S__ __S__-item">执行时间 <input className="__S__-control" type="time" value={value} onChange={(e) => onChange && onChange(e.target.value)} /></label>`,
datepicker: String.raw`<label className="__S__ __S__-item">日期 <input className="__S__-control" type="date" value={value} onChange={(e) => onChange && onChange(e.target.value)} /></label>`,
datetimepicker: String.raw`<label className="__S__ __S__-item">生效时间 <input className="__S__-control" type="datetime-local" value={value} onChange={(e) => onChange && onChange(e.target.value)} /></label>`,
form: String.raw`<form className="__S__ __S__-form" onSubmit={(e) => { e.preventDefault(); onSubmit && onSubmit(e); }}><label className="__S__-field"><span>{label} {required ? <b aria-hidden="true">*</b> : null}</span><input className="__S__-control" name="name" required={required} /></label>{error ? <span className="__S__-error">{error}</span> : null}<div className="__S__-row"><button className="__S__-button __S__-primary" type="submit">提交</button><button className="__S__-button" type="reset">重置</button></div></form>`,
pagination: String.raw`<div className="__S__ __S__-pager" aria-label="分页"><button className="__S__-page" type="button" onClick={() => onChange && onChange(Math.max(1, page - 1))}>上一页</button><button className="__S__-page is-current" type="button">{page}</button><button className="__S__-page" type="button" onClick={() => onChange && onChange(page + 1)}>下一页</button><span className="__S__-desc">共 {total} 条 · 每页 {pageSize} 条</span></div>`,
badge: String.raw`<span className="__S__ __S__-badge-host"><span className="__S__-surface" style={{ padding: 12 }}>{children || '消息'}</span><span className="__S__-badge" aria-label="未读数量">{count > 99 ? '99+' : count}</span></span>`,
avatar: String.raw`<span className="__S__ __S__-item"><span className="__S__-avatar" style={{ width: size, height: size }} aria-label={avatar}>{src ? <img src={src} alt={avatar} /> : avatar}</span><span className="__S__-status" aria-label={status}></span></span>`,
descriptions: String.raw`<table className="__S__ __S__-table"><tbody><tr><th>订单号</th><td>KOLE-20260920</td><th>状态</th><td>处理中</td></tr><tr><th>负责人</th><td>林工</td><th>更新时间</th><td>今天 09:30</td></tr></tbody></table>`,
alert: String.raw`<div className="__S__ __S__-alert" role="alert"><span><strong>{title}</strong><br /><span className="__S__-desc">{description}</span></span>{closable ? <button className="__S__-close" type="button" aria-label="关闭" onClick={onClose}>×</button> : null}</div>`,
pageheader: String.raw`<header className="__S__ __S__-pageheader"><button className="__S__-button" type="button" onClick={() => onClick && onClick()}>← {secondaryText}</button><div className="__S__-pageheader-main"><strong className="__S__-title">{title}</strong><div className="__S__-desc">{description}</div></div><button className="__S__-button __S__-primary" type="button">保存</button></header>`,
tooltip: String.raw`<div className="__S__ __S__-tooltip-wrap"><button className="__S__-button __S__-primary" type="button" aria-describedby="__S__-tip">悬停或聚焦</button><span className="__S__-tip" id="__S__-tip" role="tooltip">{tooltip}</span></div>`,
popover: String.raw`<div className="__S__ __S__-popover-wrap"><button className="__S__-button __S__-primary" type="button" aria-expanded={open} onClick={() => onClick && onClick()}>查看信息</button>{open ? <div className="__S__-popover" role="dialog">{content}</div> : null}</div>`,
divider: String.raw`<div className="__S__"><span className="__S__-desc">{title}</span><hr className="__S__-divider" /></div>`,
infinitescroll: String.raw`<div className="__S__ __S__-scroll" style={{ height }} role="feed"><div className="__S__-scroll-item">活动记录 1</div><div className="__S__-scroll-item">活动记录 2</div><button className="__S__-button" type="button" disabled={loading || status === 'done'} onClick={onLoadMore}>{loading ? '加载中…' : status === 'done' ? '已全部加载' : '加载更多'}</button></div>`,
drawer: String.raw`<><button className="__S__ __S__-button __S__-primary" type="button" onClick={() => onClick && onClick()}>打开抽屉</button>{open ? <><div className="__S__-drawer-mask" onClick={maskClosable ? onClose : undefined} /><aside className="__S__-drawer" role="dialog" aria-modal="true"><h3 className="__S__-title">{title}</h3><p className="__S__-desc">在当前页面侧向编辑,不丢失列表上下文。</p><div className="__S__-drawer-footer"><button className="__S__-button" type="button" onClick={onClose}>取消</button><button className="__S__-button __S__-primary" type="button" onClick={onClose}>确定</button></div></aside></> : null}</>`
};
return (body[d.kind] || String.raw`<div className="__S__ __S__-surface"><strong className="__S__-title">{children || '__NAME__'}</strong><p className="__S__-desc">__USAGE__</p></div>`)
.replaceAll('__S__', s).replaceAll('__NAME__', esc(d.name)).replaceAll('__USAGE__', esc(d.usage));
}
function jsxFor(d) {
return `import React, { useState } from 'react';
import './${d.prefix}.css';
export default function Kole${d.prefix}({ ${reactProps(d)} }) {
${d.kind === 'radio' || d.kind === 'checkbox' ? "const opts = options && options.length ? options : [{ label: '选项 A', value: 'a' }, { label: '选项 B', value: 'b' }];" : ''}
return (
<div className="kole-${d.slug}-root">
${reactBody(d)}
</div>
);
}
`;
}
function vue3Template(d) {
const s = `kole-${d.slug}`;
const v = d.kind === 'radio' || d.kind === 'checkbox' ? `<div class="${s}" role="${d.kind === 'radio' ? 'radiogroup' : 'group'}"><label v-for="(o, i) in optionList" :key="i" class="${s}-option"><input type="${d.kind === 'radio' ? 'radio' : 'checkbox'}" :checked="${d.kind === 'radio' ? 'props.modelValue === o.value' : 'Array.isArray(props.modelValue) && props.modelValue.includes(o.value)'}" :disabled="props.disabled" @change="change(o.value)" />{{ o.label }}</label></div>` : null;
if (v) return v;
switch (d.kind) {
case 'switch': return `<button class="${s} ${s}-switch" :class="{ 'is-on': checked }" type="button" role="switch" :aria-checked="checked" :disabled="props.disabled" @click="toggle">切换</button>`;
case 'inputnumber': return `<div class="${s} ${s}-input-row"><button class="${s}-action" type="button" @click="change(Number(props.value) - props.step)">−</button><input class="${s}-control ${s}-number" type="number" :value="props.value" :min="props.min" :max="props.max" :step="props.step" :disabled="props.disabled" @input="change(Number($event.target.value))" /><button class="${s}-action" type="button" @click="change(Number(props.value) + props.step)">+</button></div>`;
case 'timepicker': return `<label class="${s} ${s}-item">执行时间 <input class="${s}-control" type="time" :value="props.value" @input="change($event.target.value)" /></label>`;
case 'datepicker': return `<label class="${s} ${s}-item">日期 <input class="${s}-control" type="date" :value="props.value" @input="change($event.target.value)" /></label>`;
case 'datetimepicker': return `<label class="${s} ${s}-item">生效时间 <input class="${s}-control" type="datetime-local" :value="props.value" @input="change($event.target.value)" /></label>`;
case 'form': return `<form class="${s} ${s}-form" @submit.prevent="emit('submit', $event)"><label class="${s}-field"><span>{{ props.label }} <b v-if="props.required" aria-hidden="true">*</b></span><input class="${s}-control" name="name" :required="props.required" /></label><span v-if="props.error" class="${s}-error">{{ props.error }}</span><div class="${s}-row"><button class="${s}-button ${s}-primary" type="submit">提交</button><button class="${s}-button" type="reset" @click="emit('reset')">重置</button></div></form>`;
case 'pagination': return `<div class="${s} ${s}-pager" aria-label="分页"><button class="${s}-page" type="button" @click="change(Math.max(1, props.page - 1))">上一页</button><button class="${s}-page is-current" type="button">{{ props.page }}</button><button class="${s}-page" type="button" @click="change(props.page + 1)">下一页</button><span class="${s}-desc">共 {{ props.total }} 条 · 每页 {{ props.pageSize }} 条</span></div>`;
case 'alert': return `<div class="${s} ${s}-alert" :class="'is-' + props.type" role="alert"><span><strong>{{ props.title }}</strong><br /><span class="${s}-desc">{{ props.description }}</span></span><button v-if="props.closable" class="${s}-close" type="button" aria-label="关闭" @click="emit('close')">×</button></div>`;
case 'pageheader': return `<header class="${s} ${s}-pageheader"><button class="${s}-button" type="button" @click="emit('back')">← {{ props.secondaryText }}</button><div class="${s}-pageheader-main"><strong class="${s}-title">{{ props.title }}</strong><div class="${s}-desc">{{ props.description }}</div></div><button class="${s}-button ${s}-primary" type="button">保存</button></header>`;
case 'tooltip': return `<div class="${s} ${s}-tooltip-wrap"><button class="${s}-button ${s}-primary" type="button" aria-describedby="${s}-tip">悬停或聚焦</button><span class="${s}-tip" id="${s}-tip" role="tooltip">{{ props.tooltip }}</span></div>`;
case 'popover': return `<div class="${s} ${s}-popover-wrap"><button class="${s}-button ${s}-primary" type="button" :aria-expanded="open" @click="open = !open">查看信息</button><div v-if="open" class="${s}-popover" role="dialog">{{ props.content }}</div></div>`;
case 'divider': return `<div class="${s}"><span class="${s}-desc">{{ props.title }}</span><hr class="${s}-divider" :class="'is-' + props.orientation" /></div>`;
case 'infinitescroll': return `<div class="${s} ${s}-scroll" :style="{ height: props.height }" role="feed"><div class="${s}-scroll-item">活动记录 1</div><div class="${s}-scroll-item">活动记录 2</div><button class="${s}-button" type="button" :disabled="props.loading || props.status === 'done'" @click="emit('load')">{{ props.loading ? '加载中…' : props.status === 'done' ? '已全部加载' : '加载更多' }}</button></div>`;
case 'drawer': return `<div class="${s}"><button class="${s}-button ${s}-primary" type="button" @click="emit('open')">打开抽屉</button><div v-if="open" class="${s}-drawer-mask" @click.self="props.maskClosable && emit('close')"></div><aside v-if="open" class="${s}-drawer" role="dialog" aria-modal="true"><h3 class="${s}-title">{{ props.title }}</h3><p class="${s}-desc">在当前页面侧向编辑,不丢失列表上下文。</p><div class="${s}-drawer-footer"><button class="${s}-button" type="button" @click="emit('close')">取消</button><button class="${s}-button ${s}-primary" type="button" @click="emit('confirm')">确定</button></div></aside></div>`;
default: return `<div class="${s} ${s}-surface"><strong class="${s}-title">{{ props.title || '${esc(d.name)}' }}</strong><p class="${s}-desc">${esc(d.usage)}</p><slot /></div>`;
}
}
function vue3For(d) {
const emits = d.events.length ? `const emit = defineEmits([${d.events.map(jsQuote).join(', ')}]);` : 'const emit = defineEmits([]);';
const needsOpen = ['popover', 'drawer'].includes(d.kind);
const needsChecked = d.kind === 'switch';
const needsOptions = d.kind === 'radio' || d.kind === 'checkbox';
const needsChange = ['radio', 'checkbox', 'inputnumber', 'timepicker', 'datepicker', 'datetimepicker', 'pagination'].includes(d.kind);
const state = [];
if (needsOpen) state.push('const open = ref(Boolean(props.open));');
if (needsChecked) state.push('const checked = ref(Boolean(props.checked));');
if (needsOptions) state.push("const optionList = computed(() => props.options && props.options.length ? props.options : [{ label: '选项 A', value: 'a' }, { label: '选项 B', value: 'b' }]);");
const helpers = [];
if (needsChange) helpers.push("function change(value) { emit('update:modelValue', value); emit('change', value); emit('pagechange', value); }");
if (needsChecked) helpers.push("function toggle() { checked.value = !checked.value; emit('update:modelValue', checked.value); emit('change', checked.value); }");
return `<template>\n ${vue3Template(d)}\n</template>\n\n<script setup>\nimport { ref, computed } from 'vue';\nconst props = defineProps({\n${propObject(d)}\n});\n${emits}\n${state.join('\n')}\n${helpers.join('\n')}\n</script>\n\n<style src="./${d.prefix}.css"></style>\n`;
}
function vue2Template(d) {
const s = `kole-${d.slug}`;
if (d.kind === 'radio' || d.kind === 'checkbox') return `<div class="${s}" role="${d.kind === 'radio' ? 'radiogroup' : 'group'}"><label v-for="(o, i) in optionList" :key="i" class="${s}-option"><input type="${d.kind === 'radio' ? 'radio' : 'checkbox'}" :checked="${d.kind === 'radio' ? 'modelValue === o.value' : 'Array.isArray(modelValue) && modelValue.indexOf(o.value) >= 0'}" :disabled="disabled" @change="change(o.value)" />{{ o.label }}</label></div>`;
if (d.kind === 'switch') return `<button class="${s} ${s}-switch" :class="{ 'is-on': checked }" type="button" role="switch" :aria-checked="checked" :disabled="disabled" @click="toggle">切换</button>`;
if (d.kind === 'inputnumber') return `<div class="${s} ${s}-input-row"><button class="${s}-action" type="button" @click="change(Number(value) - step)">−</button><input class="${s}-control ${s}-number" type="number" :value="value" :min="min" :max="max" :step="step" :disabled="disabled" @input="change(Number($event.target.value))" /><button class="${s}-action" type="button" @click="change(Number(value) + step)">+</button></div>`;
if (['timepicker', 'datepicker', 'datetimepicker'].includes(d.kind)) return `<label class="${s} ${s}-item">选择值 <input class="${s}-control" type="${d.kind === 'timepicker' ? 'time' : d.kind === 'datepicker' ? 'date' : 'datetime-local'}" :value="value" @input="change($event.target.value)" /></label>`;
if (d.kind === 'form') return `<form class="${s} ${s}-form" @submit.prevent="$emit('submit', $event)"><label class="${s}-field"><span>{{ label }} <b v-if="required" aria-hidden="true">*</b></span><input class="${s}-control" name="name" :required="required" /></label><span v-if="error" class="${s}-error">{{ error }}</span><div class="${s}-row"><button class="${s}-button ${s}-primary" type="submit">提交</button><button class="${s}-button" type="reset" @click="$emit('reset')">重置</button></div></form>`;
if (d.kind === 'pagination') return `<div class="${s} ${s}-pager" aria-label="分页"><button class="${s}-page" type="button" @click="change(Math.max(1, page - 1))">上一页</button><button class="${s}-page is-current" type="button">{{ page }}</button><button class="${s}-page" type="button" @click="change(page + 1)">下一页</button><span class="${s}-desc">共 {{ total }} 条 · 每页 {{ pageSize }} 条</span></div>`;
if (d.kind === 'alert') return `<div class="${s} ${s}-alert" :class="'is-' + type" role="alert"><span><strong>{{ title }}</strong><br /><span class="${s}-desc">{{ description }}</span></span><button v-if="closable" class="${s}-close" type="button" aria-label="关闭" @click="$emit('close')">×</button></div>`;
if (d.kind === 'pageheader') return `<header class="${s} ${s}-pageheader"><button class="${s}-button" type="button" @click="$emit('back')">← {{ secondaryText }}</button><div class="${s}-pageheader-main"><strong class="${s}-title">{{ title }}</strong><div class="${s}-desc">{{ description }}</div></div><button class="${s}-button ${s}-primary" type="button">保存</button></header>`;
if (d.kind === 'tooltip') return `<div class="${s} ${s}-tooltip-wrap"><button class="${s}-button ${s}-primary" type="button" aria-describedby="${s}-tip">悬停或聚焦</button><span class="${s}-tip" id="${s}-tip" role="tooltip">{{ tooltip }}</span></div>`;
if (d.kind === 'popover') return `<div class="${s} ${s}-popover-wrap"><button class="${s}-button ${s}-primary" type="button" :aria-expanded="open" @click="open = !open">查看信息</button><div v-if="open" class="${s}-popover" role="dialog">{{ content }}</div></div>`;
if (d.kind === 'divider') return `<div class="${s}"><span class="${s}-desc">{{ title }}</span><hr class="${s}-divider" :class="'is-' + orientation" /></div>`;
if (d.kind === 'infinitescroll') return `<div class="${s} ${s}-scroll" :style="{ height: height }" role="feed"><div class="${s}-scroll-item">活动记录 1</div><div class="${s}-scroll-item">活动记录 2</div><button class="${s}-button" type="button" :disabled="loading || status === 'done'" @click="$emit('load')">{{ loading ? '加载中…' : status === 'done' ? '已全部加载' : '加载更多' }}</button></div>`;
if (d.kind === 'drawer') return `<div class="${s}"><button class="${s}-button ${s}-primary" type="button" @click="$emit('open')">打开抽屉</button><div v-if="open" class="${s}-drawer-mask" @click.self="maskClosable && $emit('close')"></div><aside v-if="open" class="${s}-drawer" role="dialog" aria-modal="true"><h3 class="${s}-title">{{ title }}</h3><p class="${s}-desc">在当前页面侧向编辑,不丢失列表上下文。</p><div class="${s}-drawer-footer"><button class="${s}-button" type="button" @click="$emit('close')">取消</button><button class="${s}-button ${s}-primary" type="button" @click="$emit('confirm')">确定</button></div></aside></div>`;
return `<div class="${s} ${s}-surface"><strong class="${s}-title">{{ title || '${esc(d.name)}' }}</strong><p class="${s}-desc">${esc(d.usage)}</p><slot /></div>`;
}
function vue2For(d) {
const props = propObject(d, true);
const needsOptions = d.kind === 'radio' || d.kind === 'checkbox';
const needsChecked = d.kind === 'switch';
const needsOpen = d.kind === 'popover' || d.kind === 'drawer';
const needsChange = ['radio', 'checkbox', 'inputnumber', 'timepicker', 'datepicker', 'datetimepicker', 'pagination'].includes(d.kind);
const computed = needsOptions ? "\n computed: { optionList: function () { return this.options && this.options.length ? this.options : [{ label: '选项 A', value: 'a' }, { label: '选项 B', value: 'b' }]; } }," : '';
const dataFields = [];
if (needsOpen) dataFields.push('open: false');
if (needsChecked) dataFields.push('checked: false');
const data = dataFields.length ? `\n data: function () { return { ${dataFields.join(', ')} }; },` : '';
const methods = [];
if (needsChange) methods.push(" change: function (value) { this.$emit('input', value); this.$emit('change', value); this.$emit('pagechange', value); }");
if (needsChecked) methods.push(" toggle: function () { this.checked = !this.checked; this.$emit('input', this.checked); this.$emit('change', this.checked); }");
const methodBlock = methods.length ? `\n methods: {\n${methods.join(',\n')}\n },` : '';
const emits = d.events.length ? `\n // events: ${d.events.join(', ')}` : '';
return `<template>\n ${vue2Template(d)}\n</template>\n\n<script>\nexport default {\n name: 'Kole${d.prefix}',\n props: {\n${props}\n },${data}${computed}${methodBlock}${emits}\n};\n</script>\n\n<style src="./${d.prefix}.css"></style>\n`;
}
function specText() {
const head = '# PC 常用组件补齐批次(Element UI 左侧目录对照)\n\n本批次补齐常用的后台基础、表单、数据、提示、导航和叠加层组件。实现以零运行时依赖为边界:原生日期/时间控件保留宿主能力,数据请求、权限、国际化和完整业务流程由接入方负责。\n\n';
return head + DEFS.map((d) => `### ${d.name}\n- 用途:${d.usage}\n- Anatomy:根节点 \`kole-${d.slug}\`,包含主体、状态/操作区域,交互控件保持原生语义。\n- 变体:${variantValues(d, 'variant').join('、')};具体组件按自身属性展示方向、尺寸、状态或位置。\n- 状态:default / hover / focus / disabled;含弹层或异步内容的组件另有 open、loading、done、error 状态。\n- 无障碍:提供可见焦点、键盘可达、label/role/aria 属性;对话框和抽屉关联标题,进度/状态使用可读文本。\n- 实现边界:${d.usage} 未声明的高级数据请求、服务端校验和第三方插件能力不由组件自行推断。\n`).join('\n');
}
function run() {
const indexPath = path.join(COMPONENTS, 'index.json');
const idx = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
assertFree(idx);
const registered = new Set(idx.components.map((c) => c.slug));
const additions = DEFS.filter((d) => !registered.has(d.slug)).map((d) => ({ slug: d.slug, name: d.name, tier: 'extension', confidence: 'high', specBatch: 11, specFile: '组件11.txt', frameworksPrefix: d.prefix, contract: `components/${d.slug}.json` }));
const total = idx.components.length + additions.length;
const next = { ...idx, description: `全量组件索引:登记 frameworks/ 下全部 ${total} 个已实现组件(10 个核心规范组件 + ${total - 10} 个扩展组件),并标注规格来源批次。`, components: [...idx.components, ...additions] };
atomicWrite(indexPath, JSON.stringify(next, null, 2) + '\n');
for (const d of DEFS) {
atomicWrite(path.join(COMPONENTS, `${d.slug}.json`), JSON.stringify(contract(d), null, 2) + '\n');
atomicWrite(path.join(FRAMEWORKS, `${d.prefix}.css`), cssFor(d));
atomicWrite(path.join(FRAMEWORKS, `${d.prefix}.html`), htmlFor(d));
atomicWrite(path.join(FRAMEWORKS, `${d.prefix}.jsx`), jsxFor(d));
atomicWrite(path.join(FRAMEWORKS, `${d.prefix}.vue2.vue`), vue2For(d));
atomicWrite(path.join(FRAMEWORKS, `${d.prefix}.vue3.vue`), vue3For(d));
}
const spec = specText();
atomicWrite(ROOT_SPEC, spec);
atomicWrite(LIB_SPEC, spec);
console.log(`[add-element-components] ensured ${DEFS.length} components / ${DEFS.length * 5} framework files / ${DEFS.length} contracts`);
console.log(`[add-element-components] added ${additions.length} index entries; index now ${next.components.length} components`);
}
run();