refactor: 去掉纸叠亮边——删 stack-1/stack-2 与死代码 .sem-shadow,全组件统一复用 shadow-hard/shadow-hard-lift 唯一投影配方;E2E 新增「基础组件投影统一」契约(data-comp 钩子 + 计算值全等)
No files matched your search
@@ -38,7 +38,7 @@ test.describe('全局画布', () => {
|
|||||||
test.describe('CourseCard 规格契约', () => {
|
test.describe('CourseCard 规格契约', () => {
|
||||||
test.beforeEach(async ({ page }) => await page.goto('/components.html'))
|
test.beforeEach(async ({ page }) => await page.goto('/components.html'))
|
||||||
|
|
||||||
test('2px ink 描边 + 硬投影 + 纸叠三层边', async ({ page }) => {
|
test('2px ink 描边 + 统一硬投影(单层 ink,无纸叠)', async ({ page }) => {
|
||||||
const card = page.locator('a[href="#ch1"]').first()
|
const card = page.locator('a[href="#ch1"]').first()
|
||||||
await expect(card).toBeVisible()
|
await expect(card).toBeVisible()
|
||||||
const s = await card.evaluate((el) => {
|
const s = await card.evaluate((el) => {
|
||||||
@@ -47,8 +47,23 @@ test.describe('CourseCard 规格契约', () => {
|
|||||||
})
|
})
|
||||||
expect(s.bw).toBe('2px')
|
expect(s.bw).toBe('2px')
|
||||||
expect(s.bc).toBe('rgb(17, 17, 17)')
|
expect(s.bc).toBe('rgb(17, 17, 17)')
|
||||||
expect((s.shadow.match(/rgb|#/g) ?? []).length).toBeGreaterThanOrEqual(4) // 三层纸叠+硬投影(按色值计数)
|
// 唯一配方 --shadow-hard:恰好一层 ink 硬投影(tw 变体层均为透明占位),无纸叠多层
|
||||||
expect(s.shadow).toContain('rgb(17, 17, 17)')
|
expect(s.shadow.split('rgb(17, 17, 17)').length - 1).toBe(1)
|
||||||
|
expect(s.shadow).toContain('rgb(17, 17, 17) 5px 5px 0px 0px')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('基础组件投影统一:同一 token 复用,不得各写各的', async ({ page }) => {
|
||||||
|
const { kinds, shadows } = await page.evaluate(() => {
|
||||||
|
const els = [...document.querySelectorAll('[data-comp]')]
|
||||||
|
return {
|
||||||
|
kinds: [...new Set(els.map((el) => el.dataset.comp))].sort(),
|
||||||
|
shadows: els.map((el) => getComputedStyle(el).boxShadow),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
expect(kinds).toEqual(['assign', 'course-card', 'entry-card', 'ticket']) // 四类基础组件都有钩子
|
||||||
|
expect(shadows.length).toBeGreaterThanOrEqual(6)
|
||||||
|
expect(new Set(shadows).size).toBe(1) // 计算值完全一致=样式复用
|
||||||
|
expect(shadows[0]).toContain('rgb(17, 17, 17) 5px 5px 0px 0px')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('16:9 相框:比例、3px 黑边、内阴影、章节撞色', async ({ page }) => {
|
test('16:9 相框:比例、3px 黑边、内阴影、章节撞色', async ({ page }) => {
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 172 KiB After Width: | Height: | Size: 177 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 115 KiB |
|
Before Width: | Height: | Size: 179 KiB After Width: | Height: | Size: 179 KiB |
@@ -5,7 +5,7 @@ export function EntryGuide() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap gap-5">
|
<div className="flex flex-wrap gap-5">
|
||||||
{PROFILES.map((p) => (
|
{PROFILES.map((p) => (
|
||||||
<div key={p.name} className="w-[252px] rounded-xl border-2 border-ink bg-paper p-3 shadow-[5px_5px_0_var(--color-ink)]">
|
<div key={p.name} data-comp="entry-card" className="w-[252px] rounded-xl border-2 border-ink bg-paper p-3 shadow-hard">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span
|
<span
|
||||||
className="flex h-[26px] w-[26px] items-center justify-center rounded-lg border-2 border-ink text-[14px]"
|
className="flex h-[26px] w-[26px] items-center justify-center rounded-lg border-2 border-ink text-[14px]"
|
||||||
|
|||||||
@@ -68,11 +68,11 @@ export function MemphisCard({ chapter, entry, compact = false }: { chapter: Chap
|
|||||||
<a
|
<a
|
||||||
ref={boxRef}
|
ref={boxRef}
|
||||||
href={`#ch${chapter.id}`}
|
href={`#ch${chapter.id}`}
|
||||||
|
data-comp="course-card"
|
||||||
className="group relative block rounded-[14px] border-2 border-ink bg-paper p-[11px] no-underline
|
className="group relative block rounded-[14px] border-2 border-ink bg-paper p-[11px] no-underline
|
||||||
shadow-[0_3px_0_var(--color-stack1),0_5px_0_var(--color-stack2),7px_8px_0_var(--color-ink)]
|
shadow-hard transition-transform duration-150 focus-visible:outline-none
|
||||||
transition-transform duration-150 focus-visible:outline-none
|
hover:-translate-y-[3px] hover:shadow-hard-lift
|
||||||
hover:-translate-y-[3px] hover:shadow-[0_3px_0_var(--color-stack1),0_5px_0_var(--color-stack2),10px_11px_0_var(--color-ink)]
|
focus-visible:-translate-y-[3px] focus-visible:shadow-hard-lift"
|
||||||
focus-visible:-translate-y-[3px] focus-visible:shadow-[0_3px_0_var(--color-stack1),0_5px_0_var(--color-stack2),10px_11px_0_var(--color-ink)]"
|
|
||||||
onMouseEnter={() => setLocked(true)}
|
onMouseEnter={() => setLocked(true)}
|
||||||
onMouseLeave={() => setLocked(false)}
|
onMouseLeave={() => setLocked(false)}
|
||||||
onFocus={() => setLocked(true)}
|
onFocus={() => setLocked(true)}
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import type { Chapter } from '../lib/types'
|
|||||||
export function Ticket({ chapter, done }: { chapter: Chapter; done: boolean }) {
|
export function Ticket({ chapter, done }: { chapter: Chapter; done: boolean }) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`relative flex w-[216px] items-stretch overflow-hidden rounded-xl border-2 border-ink bg-paper shadow-[5px_5px_0_var(--color-ink)] ${done ? 'ticket-pk' : ''}`}
|
data-comp="ticket"
|
||||||
|
className={`relative flex w-[216px] items-stretch overflow-hidden rounded-xl border-2 border-ink bg-paper shadow-hard ${done ? 'ticket-pk' : ''}`}
|
||||||
>
|
>
|
||||||
{/* 头部媒体块:章节色纯填充,无独立边框(由票体裁切) */}
|
{/* 头部媒体块:章节色纯填充,无独立边框(由票体裁切) */}
|
||||||
<div className="flex w-14 shrink-0 items-center justify-center text-[22px]" style={{ background: chapter.color }}>{chapter.emoji}</div>
|
<div className="flex w-14 shrink-0 items-center justify-center text-[22px]" style={{ background: chapter.color }}>{chapter.emoji}</div>
|
||||||
@@ -39,7 +40,7 @@ export function AssignBlock({
|
|||||||
miss: { how: string; looks: string }
|
miss: { how: string; looks: string }
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="w-[260px] rounded-xl border-2 border-ink bg-paper p-3 shadow-[5px_5px_0_var(--color-ink)]">
|
<div data-comp="assign" className="w-[260px] rounded-xl border-2 border-ink bg-paper p-3 shadow-hard">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<svg width="16" height="16" viewBox="0 0 18 18" aria-hidden>
|
<svg width="16" height="16" viewBox="0 0 18 18" aria-hidden>
|
||||||
<path d="M9 1 v16 M1 9 h16" stroke="var(--color-clash2b)" strokeWidth="3.5" strokeLinecap="round" />
|
<path d="M9 1 v16 M1 9 h16" stroke="var(--color-clash2b)" strokeWidth="3.5" strokeLinecap="round" />
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ const PAIRS = [
|
|||||||
const NEUTRALS = [
|
const NEUTRALS = [
|
||||||
{ c: '#111111', n: 'ink', h: '#111111 · 描边/字/硬投影' },
|
{ c: '#111111', n: 'ink', h: '#111111 · 描边/字/硬投影' },
|
||||||
{ c: '#FFF9F0', n: 'paper', h: '#FFF9F0 · 卡面/大底' },
|
{ c: '#FFF9F0', n: 'paper', h: '#FFF9F0 · 卡面/大底' },
|
||||||
{ c: '#FFFFFF', n: 'stack-1', h: '#FFFFFF · 纸叠亮边' },
|
|
||||||
{ c: '#EADFC4', n: 'stack-2', h: '#EADFC4 · 纸叠暗层' },
|
|
||||||
]
|
]
|
||||||
const CHAPTERS6 = [
|
const CHAPTERS6 = [
|
||||||
{ c: '#FF6B6B', n: 'ch1 快速开始' }, { c: '#FF8FAB', n: 'ch2 React' }, { c: '#FFD93D', n: 'ch3 更多示例' },
|
{ c: '#FF6B6B', n: 'ch1 快速开始' }, { c: '#FF8FAB', n: 'ch2 React' }, { c: '#FFD93D', n: 'ch3 更多示例' },
|
||||||
@@ -56,7 +54,7 @@ export function App() {
|
|||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
<Section label="中性阶" note="纸的世界——字、边、投影、底">
|
<Section label="中性阶" note="纸的世界——字、边、投影、底">
|
||||||
<div className="grid grid-cols-4 gap-3">{NEUTRALS.map((x) => <Chip key={x.n} {...x} />)}</div>
|
<div className="grid grid-cols-2 gap-3">{NEUTRALS.map((x) => <Chip key={x.n} {...x} />)}</div>
|
||||||
</Section>
|
</Section>
|
||||||
<Section label="章节色" note="六章六色,只进 16:9 相框与图标瓦">
|
<Section label="章节色" note="六章六色,只进 16:9 相框与图标瓦">
|
||||||
<div className="grid grid-cols-6 gap-3">{CHAPTERS6.map((x) => <Chip key={x.n} {...x} h={x.c} />)}</div>
|
<div className="grid grid-cols-6 gap-3">{CHAPTERS6.map((x) => <Chip key={x.n} {...x} h={x.c} />)}</div>
|
||||||
@@ -73,7 +71,7 @@ export function App() {
|
|||||||
<div className="h-10 w-40 rounded-lg bg-clash2a" />
|
<div className="h-10 w-40 rounded-lg bg-clash2a" />
|
||||||
</DoDont>
|
</DoDont>
|
||||||
<DoDont text="一切鲜艳以 2px ink 描边为界">
|
<DoDont text="一切鲜艳以 2px ink 描边为界">
|
||||||
<div className="h-10 w-40 rounded-lg border-2 border-ink bg-clash2a shadow-[4px_4px_0_var(--color-ink)]" />
|
<div className="h-10 w-40 rounded-lg border-2 border-ink bg-clash2a shadow-hard" />
|
||||||
</DoDont>
|
</DoDont>
|
||||||
<DoDont bad text="大底鲜艳,字被淹没">
|
<DoDont bad text="大底鲜艳,字被淹没">
|
||||||
<div className="flex h-10 w-40 items-center justify-center rounded-lg border-2 border-ink bg-clash1b text-[11px] font-bold">大底鲜艳</div>
|
<div className="flex h-10 w-40 items-center justify-center rounded-lg border-2 border-ink bg-clash1b text-[11px] font-bold">大底鲜艳</div>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export function App() {
|
|||||||
<a
|
<a
|
||||||
key={p.href}
|
key={p.href}
|
||||||
href={p.href}
|
href={p.href}
|
||||||
className="rounded-2xl border-2 border-ink bg-paper p-5 no-underline shadow-[7px_8px_0_var(--color-ink)] transition-transform hover:-translate-y-[3px]"
|
className="rounded-2xl border-2 border-ink bg-paper p-5 no-underline shadow-hard transition-transform hover:-translate-y-[3px] hover:shadow-hard-lift"
|
||||||
>
|
>
|
||||||
<b className="text-[17px] text-ink">{p.name}</b>
|
<b className="text-[17px] text-ink">{p.name}</b>
|
||||||
<p className="m-0 mt-1 text-[12.5px] text-[#6b6b6b]">{p.desc}</p>
|
<p className="m-0 mt-1 text-[12.5px] text-[#6b6b6b]">{p.desc}</p>
|
||||||
|
|||||||
@@ -13,11 +13,13 @@
|
|||||||
/* 中性阶(纸的世界) */
|
/* 中性阶(纸的世界) */
|
||||||
--color-ink: #111111;
|
--color-ink: #111111;
|
||||||
--color-paper: #fff9f0;
|
--color-paper: #fff9f0;
|
||||||
--color-stack1: #ffffff;
|
|
||||||
--color-stack2: #eadfc4;
|
|
||||||
--color-mute: #8a7a5c;
|
--color-mute: #8a7a5c;
|
||||||
--color-body: #3d3d3d;
|
--color-body: #3d3d3d;
|
||||||
|
|
||||||
|
/* 统一硬投影——唯一投影配方,所有基础组件复用(rest / 拿起) */
|
||||||
|
--shadow-hard: 5px 5px 0 var(--color-ink);
|
||||||
|
--shadow-hard-lift: 8px 8px 0 var(--color-ink);
|
||||||
|
|
||||||
/* 章节色别名 */
|
/* 章节色别名 */
|
||||||
--color-ch1: #ff6b6b;
|
--color-ch1: #ff6b6b;
|
||||||
--color-ch2: #ff8fab;
|
--color-ch2: #ff8fab;
|
||||||
@@ -27,11 +29,6 @@
|
|||||||
--color-ch6: #a78bfa;
|
--color-ch6: #a78bfa;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 语义形状通用 */
|
|
||||||
.sem-shadow {
|
|
||||||
filter: drop-shadow(0 1px 0 rgba(255, 255, 255, 0.9));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 票券打孔:两端各以 1/4 圆做差集(完成票专用)。
|
/* 票券打孔:两端各以 1/4 圆做差集(完成票专用)。
|
||||||
* mask 硬停只做形状剪切(非色彩渐变):孔真正穿透票体——露出页面底色,
|
* mask 硬停只做形状剪切(非色彩渐变):孔真正穿透票体——露出页面底色,
|
||||||
* 描边与硬投影在孔处真实断开,无需伪元素盖色近似。 */
|
* 描边与硬投影在孔处真实断开,无需伪元素盖色近似。 */
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 172 KiB After Width: | Height: | Size: 177 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 115 KiB |
|
Before Width: | Height: | Size: 179 KiB After Width: | Height: | Size: 179 KiB |