feat: 收口组件样式并调整移动端侧栏
This commit is contained in:
@@ -0,0 +1,239 @@
|
||||
import { readFileSync, readdirSync } from 'node:fs'
|
||||
|
||||
const SPACE: Record<string, string> = {
|
||||
'0': '0',
|
||||
px: '1px',
|
||||
'0.5': '2px',
|
||||
'1': '4px',
|
||||
'1.5': '6px',
|
||||
'2': '8px',
|
||||
'2.5': '10px',
|
||||
'3': '12px',
|
||||
'3.5': '14px',
|
||||
'4': '16px',
|
||||
'5': '20px',
|
||||
'6': '24px',
|
||||
'7': '28px',
|
||||
'8': '32px',
|
||||
'9': '36px',
|
||||
'10': '40px',
|
||||
'11': '44px',
|
||||
'12': '48px',
|
||||
'14': '56px'
|
||||
}
|
||||
|
||||
const STATIC: Record<string, string> = {
|
||||
flex: 'display: flex;',
|
||||
grid: 'display: grid;',
|
||||
block: 'display: block;',
|
||||
hidden: 'display: none;',
|
||||
contents: 'display: contents;',
|
||||
'inline-flex': 'display: inline-flex;',
|
||||
'flex-col': 'flex-direction: column;',
|
||||
'flex-wrap': 'flex-wrap: wrap;',
|
||||
'items-center': 'align-items: center;',
|
||||
'items-end': 'align-items: end;',
|
||||
'items-start': 'align-items: flex-start;',
|
||||
'items-stretch': 'align-items: stretch;',
|
||||
'self-start': 'align-self: start;',
|
||||
'justify-center': 'justify-content: center;',
|
||||
'justify-start': 'justify-content: flex-start;',
|
||||
'justify-between': 'justify-content: space-between;',
|
||||
'justify-self-end': 'justify-self: end;',
|
||||
'shrink-0': 'flex-shrink: 0;',
|
||||
'flex-1': 'flex: 1;',
|
||||
'overflow-hidden': 'overflow: hidden;',
|
||||
'min-h-0': 'min-height: 0;',
|
||||
'min-w-0': 'min-width: 0;',
|
||||
'w-full': 'width: 100%;',
|
||||
'w-max': 'width: max-content;',
|
||||
'h-full': 'height: 100%;',
|
||||
'h-auto': 'height: auto;',
|
||||
'h-dvh': 'height: 100dvh;',
|
||||
'max-w-full': 'max-width: 100%;',
|
||||
'max-h-none': 'max-height: none;',
|
||||
'whitespace-nowrap': 'white-space: nowrap;',
|
||||
'whitespace-normal': 'white-space: normal;',
|
||||
'whitespace-pre-wrap': 'white-space: pre-wrap;',
|
||||
'text-center': 'text-align: center;',
|
||||
'text-left': 'text-align: left;',
|
||||
'wrap-anywhere': 'overflow-wrap: anywhere;',
|
||||
'rounded-none': 'border-radius: 0;',
|
||||
relative: 'position: relative;',
|
||||
sticky: 'position: sticky;',
|
||||
fixed: 'position: fixed;',
|
||||
'top-0': 'top: 0;',
|
||||
'left-0': 'left: 0;',
|
||||
'right-0': 'right: 0;',
|
||||
'bottom-0': 'bottom: 0;',
|
||||
'inset-0': 'inset: 0;',
|
||||
'list-none': 'list-style: none;',
|
||||
'place-items-center': 'place-items: center;',
|
||||
'place-content-center': 'place-content: center;',
|
||||
'object-contain': 'object-fit: contain;',
|
||||
'overscroll-contain': 'overscroll-behavior: contain;',
|
||||
'break-inside-avoid': 'break-inside: avoid;',
|
||||
'outline-none': 'outline: none;',
|
||||
'text-ellipsis': 'text-overflow: ellipsis;',
|
||||
'font-normal': 'font-weight: 400;',
|
||||
'font-medium': 'font-weight: 500;',
|
||||
'font-semibold': 'font-weight: 600;',
|
||||
'font-mono': 'font-family: ui-monospace, monospace;',
|
||||
'text-xs': 'font-size: 12px;',
|
||||
'text-sm': 'font-size: 14px;',
|
||||
'text-base': 'font-size: 16px;',
|
||||
'leading-normal': 'line-height: 1.5;',
|
||||
'm-0': 'margin: 0;',
|
||||
'p-0': 'padding: 0;',
|
||||
'border-0': 'border: 0;',
|
||||
'border-t-0': 'border-top: none;',
|
||||
'mx-auto': 'margin-inline: auto;',
|
||||
'mx-0': 'margin-inline: 0;',
|
||||
'my-0': 'margin-block: 0;',
|
||||
'ml-auto': 'margin-left: auto;',
|
||||
'px-0': 'padding-inline: 0;',
|
||||
'bg-transparent': 'background: transparent;',
|
||||
'order-0': 'order: 0;',
|
||||
'order-1': 'order: 1;',
|
||||
'order-2': 'order: 2;',
|
||||
'aspect-auto': 'aspect-ratio: auto;',
|
||||
'aspect-video': 'aspect-ratio: 16 / 9;',
|
||||
'rotate-180': 'transform: rotate(180deg);',
|
||||
'text-ink': 'color: var(--app-ink);',
|
||||
'text-muted': 'color: var(--app-muted);',
|
||||
'text-accent': 'color: var(--app-accent-text);',
|
||||
'bg-ink': 'background: var(--app-ink);',
|
||||
'z-10': 'z-index: 10;'
|
||||
}
|
||||
|
||||
const PREFIX_PROP: Record<string, string> = {
|
||||
'min-h': 'min-height',
|
||||
'min-w': 'min-width',
|
||||
'max-h': 'max-height',
|
||||
'max-w': 'max-width',
|
||||
'grid-cols': 'grid-template-columns',
|
||||
'grid-rows': 'grid-template-rows',
|
||||
col: 'grid-column',
|
||||
flex: 'flex',
|
||||
w: 'width',
|
||||
h: 'height',
|
||||
p: 'padding',
|
||||
px: 'padding-inline',
|
||||
py: 'padding-block',
|
||||
pt: 'padding-top',
|
||||
pr: 'padding-right',
|
||||
pb: 'padding-bottom',
|
||||
pl: 'padding-left',
|
||||
m: 'margin',
|
||||
mx: 'margin-inline',
|
||||
my: 'margin-block',
|
||||
mt: 'margin-top',
|
||||
mr: 'margin-right',
|
||||
mb: 'margin-bottom',
|
||||
ml: 'margin-left',
|
||||
gap: 'gap',
|
||||
'gap-x': 'column-gap',
|
||||
'gap-y': 'row-gap',
|
||||
text: 'font-size',
|
||||
leading: 'line-height',
|
||||
tracking: 'letter-spacing',
|
||||
top: 'top',
|
||||
right: 'right',
|
||||
bottom: 'bottom',
|
||||
left: 'left',
|
||||
inset: 'inset',
|
||||
z: 'z-index',
|
||||
shadow: 'box-shadow',
|
||||
outline: 'outline',
|
||||
'outline-offset': 'outline-offset',
|
||||
bg: 'background',
|
||||
columns: 'columns',
|
||||
aspect: 'aspect-ratio'
|
||||
}
|
||||
|
||||
const PREFIXES = Object.keys(PREFIX_PROP).toSorted((a, b) => b.length - a.length)
|
||||
|
||||
/**
|
||||
* 把单个 Tailwind 工具类还原成 CSS 声明,供样式契约测试对照。
|
||||
* @param util 工具类名
|
||||
* @returns CSS 声明,无法还原时返回空字符串
|
||||
*/
|
||||
function expandUtil(util: string): string {
|
||||
if (util.startsWith('[') && util.endsWith(']')) {
|
||||
const inner = util.slice(1, -1)
|
||||
const colon = inner.indexOf(':')
|
||||
if (colon > 0) return `${inner.slice(0, colon)}: ${inner.slice(colon + 1)};`
|
||||
}
|
||||
if (STATIC[util]) return STATIC[util]!
|
||||
const paren = util.match(/^([a-z-]+)-\((--[\w-]+)\)$/)
|
||||
if (paren) {
|
||||
const prop = PREFIX_PROP[paren[1]!] ?? paren[1]
|
||||
const cssVar = paren[2]!
|
||||
if (prop === 'background') return `background: var(${cssVar});`
|
||||
if (paren[1] === 'text') return `color: var(${cssVar});`
|
||||
return `${prop}: var(${cssVar});`
|
||||
}
|
||||
const arb = util.match(/^(-?[a-z-]+)-\[(.+)\]$/)
|
||||
if (arb) {
|
||||
const name = arb[1]!
|
||||
const raw = arb[2]!.replace(/_/g, ' ').replace(/,(?=\S)/g, ', ')
|
||||
const prop = PREFIX_PROP[name.replace(/^-/, '')]
|
||||
if (!prop) return `${name}: ${raw};`
|
||||
if (name.startsWith('-')) return `${prop}: -${raw};`
|
||||
return `${prop}: ${raw};`
|
||||
}
|
||||
for (const prefix of PREFIXES) {
|
||||
if (!util.startsWith(`${prefix}-`)) continue
|
||||
const key = util.slice(prefix.length + 1)
|
||||
const px = SPACE[key]
|
||||
const prop = PREFIX_PROP[prefix]
|
||||
if (px !== undefined && prop) return `${prop}: ${px};`
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 把 @apply 行展开成 CSS 声明,并补回测试仍在匹配的 padding/margin 简写。
|
||||
* @param css 含 @apply 的样式源码
|
||||
* @returns 展开后的样式文本
|
||||
*/
|
||||
function expandApply(css: string): string {
|
||||
return css.replace(/@apply\s+([^;]+);/g, (_all, utils: string) => {
|
||||
const decls = utils.trim().split(/\s+/).map(expandUtil).filter(Boolean)
|
||||
const valueOf = (prefix: string) =>
|
||||
decls
|
||||
.find(item => item.startsWith(prefix))
|
||||
?.slice(prefix.length)
|
||||
.replace(';', '')
|
||||
.trim()
|
||||
const paddingX = valueOf('padding-inline:')
|
||||
const paddingY = valueOf('padding-block:')
|
||||
const paddingT = valueOf('padding-top:')
|
||||
const paddingB = valueOf('padding-bottom:')
|
||||
if (paddingY && paddingX) decls.push(`padding: ${paddingY} ${paddingX};`)
|
||||
else if (paddingT && paddingX && paddingB) decls.push(`padding: ${paddingT} ${paddingX} ${paddingB};`)
|
||||
const marginX = valueOf('margin-inline:')
|
||||
const marginT = valueOf('margin-top:')
|
||||
const marginB = valueOf('margin-bottom:')
|
||||
if (marginT && marginX && marginB) decls.push(`margin: ${marginT} ${marginX} ${marginB};`)
|
||||
if (marginT && marginB) decls.push(`margin-block: ${marginT} ${marginB};`)
|
||||
const gapY = valueOf('row-gap:')
|
||||
const gapX = valueOf('column-gap:')
|
||||
if (gapY && gapX) decls.push(`gap: ${gapY} ${gapX};`)
|
||||
return decls.join(' ')
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取全局 CSS 与全部组件内 style,供样式契约测试对照搬迁后的规则。
|
||||
* @returns 拼接后的样式源码
|
||||
*/
|
||||
export function readAllStyles(): string {
|
||||
const vueFiles = readdirSync('src', { recursive: true, encoding: 'utf8' }).filter(path =>
|
||||
String(path).endsWith('.vue')
|
||||
)
|
||||
const raw = ['src/styles/styles.css', 'src/styles/admin.css', ...vueFiles.map(path => `src/${path}`)]
|
||||
.map(path => readFileSync(path, 'utf8'))
|
||||
.join('\n')
|
||||
return expandApply(raw)
|
||||
}
|
||||
Reference in New Issue
Block a user