Centralize interactive component styles, adopt the fresh eucalyptus palette, and keep map controls square with hard shadows. Update project guidance and the OpenCloud style skill to match the implementation.
105 lines
3.5 KiB
JavaScript
105 lines
3.5 KiB
JavaScript
import { readdir, readFile } from 'node:fs/promises'
|
|
import { extname, join, relative } from 'node:path'
|
|
|
|
const projectRoot = new URL('..', import.meta.url)
|
|
const sourceRoot = new URL('../src', import.meta.url)
|
|
|
|
const deprecatedPatterns = [
|
|
{
|
|
pattern: /\b(?:bg|border|ring|text)-(?:gray|indigo|purple|violet)-\d+\b/g,
|
|
message: '使用项目约定的 slate/sky/自然绿色语义色,不要重新引入灰色或紫色系。',
|
|
},
|
|
{
|
|
pattern: /\b(?:bg|border|ring|text)-(?:green|lime)-\d+\b/g,
|
|
message: '绿色必须使用项目重映射后的 teal 品牌色或 emerald 成功色,不要引入默认 green/lime 荧光色。',
|
|
},
|
|
{
|
|
pattern: /#(?:f0fdfa|ccfbf1|99f6e4|5eead4|2dd4bf|14b8a6|0d9488|0f766e|115e59|134e4a)\b/gi,
|
|
message: '检测到旧版高饱和 teal 色值,请改用自然绿色设计令牌。',
|
|
},
|
|
{
|
|
pattern: /text-xs uppercase tracking-\[0\.18em\] text-slate-500/g,
|
|
message: '重复的小节标签应使用 oc-section-label。',
|
|
vueOnly: true,
|
|
},
|
|
{
|
|
pattern: /border border-slate-200 bg-slate-50 p-4/g,
|
|
message: '标准内嵌信息面板应使用 oc-inset-panel。',
|
|
vueOnly: true,
|
|
},
|
|
{
|
|
pattern: /border-b border-sky-100 bg-\[linear-gradient\(180deg,#e0f2fe_0%,#f8fafc_100%\)\]/g,
|
|
message: '标准页面头部应使用 oc-page-hero。',
|
|
vueOnly: true,
|
|
},
|
|
]
|
|
|
|
const styleSourceExtensions = new Set(['.vue', '.css', '.ts'])
|
|
|
|
async function collectStyleSourceFiles(directory) {
|
|
const entries = await readdir(directory, { withFileTypes: true })
|
|
const files = await Promise.all(entries.map(async (entry) => {
|
|
const path = join(directory, entry.name)
|
|
return entry.isDirectory() ? collectStyleSourceFiles(path) : [path]
|
|
}))
|
|
|
|
return files.flat().filter((path) => styleSourceExtensions.has(extname(path)))
|
|
}
|
|
|
|
function lineNumberAt(source, index) {
|
|
return source.slice(0, index).split('\n').length
|
|
}
|
|
|
|
const violations = []
|
|
|
|
for (const file of await collectStyleSourceFiles(sourceRoot.pathname)) {
|
|
const source = await readFile(file, 'utf8')
|
|
|
|
for (const rule of deprecatedPatterns) {
|
|
if (rule.vueOnly && extname(file) !== '.vue') continue
|
|
|
|
for (const match of source.matchAll(rule.pattern)) {
|
|
violations.push({
|
|
file: relative(projectRoot.pathname, file),
|
|
line: lineNumberAt(source, match.index),
|
|
message: rule.message,
|
|
value: match[0],
|
|
})
|
|
}
|
|
}
|
|
|
|
for (const match of source.matchAll(/(?:^|\s)class="([^"]+)"/gm)) {
|
|
const classes = match[1].split(/\s+/)
|
|
const panelModifier = classes.some((name) => name.startsWith('oc-panel-button--'))
|
|
const primaryModifier = classes.some((name) => name.startsWith('oc-primary-button--'))
|
|
|
|
if (panelModifier && !classes.includes('oc-panel-button')) {
|
|
violations.push({
|
|
file: relative(projectRoot.pathname, file),
|
|
line: lineNumberAt(source, match.index),
|
|
message: 'oc-panel-button 修饰类必须与基础类一起使用。',
|
|
value: match[0],
|
|
})
|
|
}
|
|
|
|
if (primaryModifier && !classes.includes('oc-primary-button')) {
|
|
violations.push({
|
|
file: relative(projectRoot.pathname, file),
|
|
line: lineNumberAt(source, match.index),
|
|
message: 'oc-primary-button 修饰类必须与基础类一起使用。',
|
|
value: match[0],
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
if (violations.length > 0) {
|
|
console.error('样式体系检查失败:')
|
|
for (const violation of violations) {
|
|
console.error(`- ${violation.file}:${violation.line} ${violation.message} (${violation.value})`)
|
|
}
|
|
process.exitCode = 1
|
|
} else {
|
|
console.log('样式体系检查通过。')
|
|
}
|