feat: 移除测试 保持原始功能纯粹
This commit is contained in:
@@ -1,58 +1,42 @@
|
||||
---
|
||||
name: add-test-page
|
||||
description: Add a standalone feature validation page to this repo when a new capability needs a demo, smoke test, or isolated basic functional verification.
|
||||
description: Create a temporary standalone validation page on a spike branch when architecture or feature behavior needs isolated visual verification.
|
||||
---
|
||||
|
||||
Use this skill when the user asks to add a standalone validation page, demo route, smoke test page, or a basic feature verification page.
|
||||
Use this skill when the user asks to add a standalone validation page, demo route, smoke test page, or an isolated visual verification surface.
|
||||
|
||||
## Read First
|
||||
## Branch Rule
|
||||
|
||||
- `src/features/test-lab/registry.ts`
|
||||
- `src/features/test-lab/components/TestLabShell.vue`
|
||||
- `src/features/test-lab/pages/TailwindV4TestPage.vue`
|
||||
- `src/features/test-lab/__tests__/registry.spec.ts`
|
||||
- Do not add architecture validation pages directly on the main product branch.
|
||||
- Prefer a dedicated spike branch such as `spike/tailwind-v4-lab` or `spike/router-layout-check`.
|
||||
- If the current branch is intended for production-facing work, move the validation work to a spike branch before adding temporary pages or routes.
|
||||
|
||||
## Repo Contract
|
||||
|
||||
- The home route is not a test page. Keep `/` reserved for the product-facing homepage or dashboard.
|
||||
- Keep the shell generic. Do not add feature-specific content to `src/App.vue` or `src/features/test-lab/components/TestLabShell.vue`.
|
||||
- Create one page component per feature under `src/features/test-lab/pages/<FeatureName>TestPage.vue`.
|
||||
- Register the page in `src/features/test-lab/registry.ts`. Routes are generated from the registry, so the slug becomes the URL name.
|
||||
- Test pages must live under `/lab/<slug>` and stay isolated from homepage concerns.
|
||||
- Temporary validation routes must not become the default entry path of the app.
|
||||
- Keep experiment code visibly isolated under a temporary area such as `src/spikes/<topic>/`.
|
||||
- Name temporary routes clearly, for example `/__spike/tailwind-v4` or `/__spike/router-layout`.
|
||||
- Keep the page self-contained. Use inline mock data or local state instead of real APIs unless the feature explicitly requires integration verification.
|
||||
- Add or update tests so the registry contract and the new page's key text remain discoverable by Vitest.
|
||||
- Add only the smallest tests needed to lock the intended architecture rule or render contract.
|
||||
- Before merging back, delete one-off validation pages and routes unless they provide lasting regression value.
|
||||
|
||||
## Required Metadata
|
||||
## What Can Merge Back
|
||||
|
||||
Every registry item must provide:
|
||||
|
||||
- `slug`: unique kebab-case identifier
|
||||
- `title`: short feature name
|
||||
- `summary`: one-line scope statement
|
||||
- `goal`: explicit validation target
|
||||
- `checks`: at least 3 visible requirements
|
||||
- `manualChecks`: at least 2 browser actions or observations
|
||||
- `status`: `ready` or `draft`
|
||||
- `component`: the Vue page component
|
||||
|
||||
## Page Shape
|
||||
|
||||
Each test page should include these sections:
|
||||
|
||||
1. A hero block that states what is being validated.
|
||||
2. A live demo area that exposes the core feature.
|
||||
3. Evidence cards for key states, variants, or edge cases.
|
||||
4. A checklist area that mirrors `checks` and `manualChecks`.
|
||||
5. A success criterion that says what counts as "working".
|
||||
- Skill updates
|
||||
- Reusable scaffolding
|
||||
- Naming and routing conventions
|
||||
- Minimal contract tests that protect the final architecture
|
||||
- Documentation or comments that clarify the validated rule
|
||||
|
||||
## Implementation Rules
|
||||
|
||||
- Make the verification visual. A test page is not a prose document.
|
||||
- Prefer deterministic state toggles so the reviewer can force each state on demand.
|
||||
- If the feature introduces styles or tokens, show the exact utility classes in the page.
|
||||
- If the feature introduces styles or tokens, show the exact utility classes in the spike page.
|
||||
- If the feature introduces interaction, include hover, focus, active, or toggled states.
|
||||
- If the feature introduces responsive behavior, include a breakpoint probe.
|
||||
- If the feature is not ready, keep the page status at `draft` instead of hiding incomplete coverage.
|
||||
- End the spike by extracting conclusions, then remove temporary assets from the merge candidate.
|
||||
|
||||
## Validation
|
||||
|
||||
|
||||
@@ -20,10 +20,10 @@ describe('App', () => {
|
||||
|
||||
expect(router.currentRoute.value.name).toBe('home')
|
||||
expect(wrapper.text()).toContain('Agentic Vue3 Forge')
|
||||
expect(wrapper.text()).toContain('Open Validation Lab')
|
||||
expect(wrapper.text()).toContain('Mainline Ready')
|
||||
})
|
||||
|
||||
it('keeps the validation page under the lab route', async () => {
|
||||
it('does not keep architecture validation routes in the main branch', async () => {
|
||||
const router = createAppRouter(createMemoryHistory())
|
||||
const wrapper = mount(App, {
|
||||
global: {
|
||||
@@ -34,8 +34,7 @@ describe('App', () => {
|
||||
await router.push('/lab')
|
||||
await flushPromises()
|
||||
|
||||
expect(router.currentRoute.value.name).toBe('tailwind-v4')
|
||||
expect(wrapper.text()).toContain('Tailwind CSS v4 smoke test')
|
||||
expect(wrapper.text()).toContain('Utilities should feel obvious.')
|
||||
expect(router.currentRoute.value.name).toBe('home')
|
||||
expect(wrapper.text()).toContain('spike 分支')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,27 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import { RouterLink } from 'vue-router'
|
||||
|
||||
import { defaultFeatureTestPage } from '@/features/test-lab/registry'
|
||||
|
||||
const operatingPrinciples = [
|
||||
{
|
||||
title: 'Structure before scale',
|
||||
detail: '先把目录、路由和约束立住,再往里持续填模块。',
|
||||
title: 'Primary branch stays clean',
|
||||
detail: '当前分支只放正式架构和业务代码,不承接一次性验证页面。',
|
||||
},
|
||||
{
|
||||
title: 'Visible validation',
|
||||
detail: '每个新功能都能落到单独的实验页,不把验证逻辑混进业务首页。',
|
||||
title: 'Spike first for architecture',
|
||||
detail: '框架、样式链路、目录试验先去 spike 分支验证,确认后只回收规则沉淀。',
|
||||
},
|
||||
{
|
||||
title: 'AI-friendly clarity',
|
||||
detail: '命名、边界和测试都保持可搜索、可接手、可回归。',
|
||||
title: 'Keep only reusable residue',
|
||||
detail: '合回主线的应该是 skill、约定、脚手架和必要 contract test,而不是实验页面。',
|
||||
},
|
||||
]
|
||||
|
||||
const nextSteps = [
|
||||
'补业务首页模块和真实导航',
|
||||
'按功能继续扩展 test-lab 页面注册表',
|
||||
'逐步补上 query、store、api 的最小测试',
|
||||
'补正式首页导航和业务模块入口',
|
||||
'为核心模块定义最小 contract test',
|
||||
'把架构验证流程固化到 spike 分支约定里',
|
||||
]
|
||||
</script>
|
||||
|
||||
@@ -44,22 +40,21 @@ const nextSteps = [
|
||||
<p class="text-xs uppercase tracking-[0.36em] text-ink/45">Agentic Vue3 Forge</p>
|
||||
<div class="space-y-4">
|
||||
<h1 class="max-w-4xl font-display text-5xl leading-none text-balance sm:text-6xl">
|
||||
Vue3 工程底座先跑起来,验证页留在侧边战场。
|
||||
主分支先保持纯净,架构验证放到独立 spike 分支。
|
||||
</h1>
|
||||
<p class="max-w-2xl text-sm leading-7 text-ink/68 sm:text-base">
|
||||
首页先保持干净,承载项目定位和后续导航。功能验证统一放进 test
|
||||
lab,避免首页被临时实验内容侵占。
|
||||
这个首页先承担项目定位和正式入口。临时实验、样式验证、路由试探都不直接留在当前分支,
|
||||
只把最终确认过的规则和可复用结构带回来。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<RouterLink
|
||||
:to="{ name: defaultFeatureTestPage.slug }"
|
||||
class="inline-flex items-center justify-center rounded-full bg-ink px-5 py-3 text-sm uppercase tracking-[0.24em] text-paper transition hover:-translate-y-0.5 hover:bg-ink/92 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ink/20"
|
||||
<span
|
||||
class="inline-flex items-center justify-center rounded-full bg-ink px-5 py-3 text-sm uppercase tracking-[0.24em] text-paper"
|
||||
>
|
||||
Open Validation Lab
|
||||
</RouterLink>
|
||||
Mainline Ready
|
||||
</span>
|
||||
<a
|
||||
href="https://vuejs.org/"
|
||||
target="_blank"
|
||||
@@ -96,10 +91,9 @@ const nextSteps = [
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-[24px] border border-paper/12 bg-paper/8 p-4">
|
||||
<p class="font-display text-3xl leading-none text-paper">Homepage stays clean</p>
|
||||
<p class="font-display text-3xl leading-none text-paper">Spike-driven validation</p>
|
||||
<p class="mt-3 text-sm leading-7 text-paper/68">
|
||||
业务入口与验证入口分离。首页以后可以换成正式 landing 或
|
||||
dashboard,不影响测试路由。
|
||||
架构试验不常驻主线。需要 smoke page 时,去 `spike/*` 分支开临时页面和路由。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { featureTestPages } from '@/features/test-lab/registry'
|
||||
|
||||
describe('feature test page registry', () => {
|
||||
it('contains at least one page', () => {
|
||||
expect(featureTestPages.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('enforces the test page contract', () => {
|
||||
const seenSlugs = new Set<string>()
|
||||
|
||||
for (const page of featureTestPages) {
|
||||
expect(page.slug).toMatch(/^[a-z0-9]+(?:-[a-z0-9]+)*$/)
|
||||
expect(seenSlugs.has(page.slug)).toBe(false)
|
||||
expect(page.title.trim()).not.toBe('')
|
||||
expect(page.summary.trim()).not.toBe('')
|
||||
expect(page.goal.trim()).not.toBe('')
|
||||
expect(page.status).toMatch(/^(ready|draft)$/)
|
||||
expect(page.component).toBeTruthy()
|
||||
expect(page.checks.length).toBeGreaterThanOrEqual(3)
|
||||
expect(page.manualChecks.length).toBeGreaterThanOrEqual(2)
|
||||
|
||||
for (const item of [...page.checks, ...page.manualChecks]) {
|
||||
expect(item.trim()).not.toBe('')
|
||||
}
|
||||
|
||||
seenSlugs.add(page.slug)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -1,139 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { RouterLink, RouterView, useRoute } from 'vue-router'
|
||||
|
||||
import { featureTestPages, type FeatureTestPage } from '@/features/test-lab/registry'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const currentPage = computed(() => route.meta.page as FeatureTestPage | undefined)
|
||||
const currentRouteName = computed(() => route.name?.toString() ?? '')
|
||||
|
||||
const labRules = [
|
||||
{
|
||||
title: '独立成页',
|
||||
description: '每个新功能单独放一个验证页,不把实验代码混进业务组件。',
|
||||
},
|
||||
{
|
||||
title: '覆盖关键态',
|
||||
description: '至少展示默认态、交互态、响应式或边界态中的三类证据。',
|
||||
},
|
||||
{
|
||||
title: '配最小单测',
|
||||
description: '注册表和关键文案要能被 Vitest 扫到,防止约定失效。',
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-screen">
|
||||
<div
|
||||
class="mx-auto flex min-h-screen max-w-7xl flex-col gap-5 px-4 py-4 sm:px-6 lg:flex-row lg:px-8 lg:py-8"
|
||||
>
|
||||
<aside
|
||||
class="flex w-full shrink-0 flex-col gap-5 rounded-[32px] border border-paper/15 bg-paper/8 p-5 shadow-panel backdrop-blur-sm lg:w-[320px]"
|
||||
>
|
||||
<div class="space-y-3">
|
||||
<p class="text-xs uppercase tracking-[0.36em] text-paper/55">Forge Validation Lab</p>
|
||||
<div class="space-y-2">
|
||||
<h1 class="font-display text-3xl leading-none text-paper sm:text-4xl">功能验证台</h1>
|
||||
<p class="max-w-sm text-sm leading-6 text-paper/70">
|
||||
为每个新能力留一个可视化、可交互、可回归的最小验证页。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="space-y-3">
|
||||
<RouterLink
|
||||
v-for="page in featureTestPages"
|
||||
:key="page.slug"
|
||||
:to="{ name: page.slug }"
|
||||
class="group block rounded-[24px] border px-4 py-4 transition duration-200"
|
||||
:class="
|
||||
currentRouteName === page.slug
|
||||
? 'border-signal/45 bg-signal/16 text-paper'
|
||||
: 'border-paper/12 bg-paper/6 text-paper/70 hover:border-paper/25 hover:bg-paper/10 hover:text-paper'
|
||||
"
|
||||
>
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="space-y-2">
|
||||
<p class="font-display text-xl leading-none">{{ page.title }}</p>
|
||||
<p class="text-sm leading-6 opacity-80">{{ page.summary }}</p>
|
||||
</div>
|
||||
<span
|
||||
class="rounded-full border px-2.5 py-1 text-[11px] uppercase tracking-[0.28em]"
|
||||
:class="
|
||||
page.status === 'ready'
|
||||
? 'border-signal/35 bg-signal/12 text-paper'
|
||||
: 'border-clay/30 bg-clay/15 text-paper/80'
|
||||
"
|
||||
>
|
||||
{{ page.status }}
|
||||
</span>
|
||||
</div>
|
||||
</RouterLink>
|
||||
</nav>
|
||||
|
||||
<section class="space-y-3 rounded-[28px] border border-paper/12 bg-ink/35 p-4">
|
||||
<p class="text-xs uppercase tracking-[0.32em] text-paper/45">规则</p>
|
||||
<div
|
||||
v-for="rule in labRules"
|
||||
:key="rule.title"
|
||||
class="rounded-[20px] border border-paper/8 bg-paper/6 p-3"
|
||||
>
|
||||
<p class="font-display text-lg text-paper">{{ rule.title }}</p>
|
||||
<p class="mt-1 text-sm leading-6 text-paper/70">{{ rule.description }}</p>
|
||||
</div>
|
||||
</section>
|
||||
</aside>
|
||||
|
||||
<main class="flex min-w-0 flex-1 flex-col gap-5">
|
||||
<header
|
||||
class="overflow-hidden rounded-[32px] border border-paper/14 bg-paper/90 text-ink shadow-panel"
|
||||
>
|
||||
<div
|
||||
class="grid gap-5 p-6 sm:p-8 xl:grid-cols-[minmax(0,1.2fr)_minmax(320px,0.8fr)] xl:items-end"
|
||||
>
|
||||
<div class="space-y-4">
|
||||
<p class="text-xs uppercase tracking-[0.34em] text-ink/45">
|
||||
Current Validation Target
|
||||
</p>
|
||||
<div class="space-y-3">
|
||||
<h2 class="font-display text-4xl leading-none text-balance sm:text-5xl">
|
||||
{{ currentPage?.title ?? 'No page selected' }}
|
||||
</h2>
|
||||
<p class="max-w-2xl text-sm leading-7 text-ink/70 sm:text-base">
|
||||
{{
|
||||
currentPage?.goal ?? 'Add a page in the registry to start validating a feature.'
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3 sm:grid-cols-3 xl:grid-cols-1">
|
||||
<div class="rounded-[24px] border border-ink/10 bg-ink/5 p-4">
|
||||
<p class="text-xs uppercase tracking-[0.28em] text-ink/45">Pages</p>
|
||||
<p class="mt-3 font-display text-3xl">{{ featureTestPages.length }}</p>
|
||||
<p class="mt-1 text-sm text-ink/55">一个功能,一张验证页。</p>
|
||||
</div>
|
||||
<div class="rounded-[24px] border border-ink/10 bg-ink/5 p-4">
|
||||
<p class="text-xs uppercase tracking-[0.28em] text-ink/45">Checks</p>
|
||||
<p class="mt-3 font-display text-3xl">{{ currentPage?.checks.length ?? 0 }}</p>
|
||||
<p class="mt-1 text-sm text-ink/55">默认要点,直接可见。</p>
|
||||
</div>
|
||||
<div class="rounded-[24px] border border-ink/10 bg-ink/5 p-4">
|
||||
<p class="text-xs uppercase tracking-[0.28em] text-ink/45">Status</p>
|
||||
<p class="mt-3 font-display text-3xl uppercase">
|
||||
{{ currentPage?.status ?? 'draft' }}
|
||||
</p>
|
||||
<p class="mt-1 text-sm text-ink/55">准备好后再并入业务页。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<RouterView />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,403 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
import type { FeatureTestPage } from '@/features/test-lab/registry'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const page = computed(() => route.meta.page as FeatureTestPage | undefined)
|
||||
|
||||
const density = ref<'comfortable' | 'compact'>('comfortable')
|
||||
const tone = ref<'signal' | 'clay'>('signal')
|
||||
const showOverlay = ref(true)
|
||||
|
||||
const densityLabel = computed(() =>
|
||||
density.value === 'comfortable' ? 'Comfortable spacing' : 'Compact spacing',
|
||||
)
|
||||
|
||||
const framePaddingClass = computed(() =>
|
||||
density.value === 'comfortable' ? 'gap-5 p-6 sm:p-7' : 'gap-3 p-4 sm:p-5',
|
||||
)
|
||||
|
||||
const previewSurfaceClass = computed(() =>
|
||||
tone.value === 'signal'
|
||||
? 'bg-[linear-gradient(135deg,rgba(111,188,220,0.18),rgba(247,244,238,0.96))]'
|
||||
: 'bg-[linear-gradient(135deg,rgba(197,143,99,0.18),rgba(247,244,238,0.96))]',
|
||||
)
|
||||
|
||||
const progressValue = computed(() => (density.value === 'comfortable' ? 86 : 68))
|
||||
|
||||
const probeCards = [
|
||||
{
|
||||
title: 'Typography token',
|
||||
detail: 'font-display + tracking-[0.32em]',
|
||||
note: '标题与正文应该有明显的气质差异,不是同一套默认字重。',
|
||||
},
|
||||
{
|
||||
title: 'State variants',
|
||||
detail: 'hover / focus-visible / peer-checked / group-hover',
|
||||
note: '悬浮、聚焦和切换时必须给出清晰反馈,方便肉眼判断状态类是否编译成功。',
|
||||
},
|
||||
{
|
||||
title: 'Arbitrary values',
|
||||
detail: 'rounded-[32px] + bg-[linear-gradient(...)]',
|
||||
note: '这类任意值最容易暴露扫描和构建问题,验证页里必须显式出现。',
|
||||
},
|
||||
]
|
||||
|
||||
const responsiveProbes = [
|
||||
{
|
||||
label: 'Spacing',
|
||||
hint: 'px-4 sm:px-6 xl:px-8',
|
||||
},
|
||||
{
|
||||
label: 'Grid',
|
||||
hint: 'md:grid-cols-2 xl:grid-cols-4',
|
||||
},
|
||||
{
|
||||
label: 'Type',
|
||||
hint: 'text-sm sm:text-base',
|
||||
},
|
||||
{
|
||||
label: 'Visibility',
|
||||
hint: 'hidden sm:inline-flex',
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<article v-if="page" class="grid gap-5">
|
||||
<section class="grid gap-5 xl:grid-cols-[minmax(0,1.15fr)_minmax(340px,0.85fr)]">
|
||||
<div
|
||||
class="rounded-[32px] border border-paper/14 bg-paper/92 p-6 text-ink shadow-panel sm:p-8"
|
||||
>
|
||||
<div class="space-y-6">
|
||||
<div class="space-y-3">
|
||||
<p class="text-xs uppercase tracking-[0.34em] text-ink/45">
|
||||
Tailwind CSS v4 smoke test
|
||||
</p>
|
||||
<div class="space-y-3">
|
||||
<h3 class="font-display text-4xl leading-none text-balance sm:text-5xl">
|
||||
Tailwind v4 is wired through Vite.
|
||||
</h3>
|
||||
<p class="max-w-3xl text-sm leading-7 text-ink/70 sm:text-base">
|
||||
{{ page.summary }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<span
|
||||
class="rounded-full border border-ink/10 bg-ink/5 px-3 py-1 text-xs uppercase tracking-[0.24em]"
|
||||
>
|
||||
font-display
|
||||
</span>
|
||||
<span
|
||||
class="rounded-full border border-ink/10 bg-ink/5 px-3 py-1 text-xs uppercase tracking-[0.24em]"
|
||||
>
|
||||
text-signal
|
||||
</span>
|
||||
<span
|
||||
class="rounded-full border border-ink/10 bg-ink/5 px-3 py-1 text-xs uppercase tracking-[0.24em]"
|
||||
>
|
||||
shadow-panel
|
||||
</span>
|
||||
<span
|
||||
class="rounded-full border border-ink/10 bg-ink/5 px-3 py-1 text-xs uppercase tracking-[0.24em]"
|
||||
>
|
||||
rounded-[32px]
|
||||
</span>
|
||||
<span
|
||||
class="rounded-full border border-ink/10 bg-ink/5 px-3 py-1 text-xs uppercase tracking-[0.24em]"
|
||||
>
|
||||
peer-checked
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3 lg:grid-cols-[repeat(2,minmax(0,1fr))_minmax(220px,0.9fr)]">
|
||||
<div class="rounded-[24px] border border-ink/10 bg-ink/5 p-4">
|
||||
<p class="text-xs uppercase tracking-[0.28em] text-ink/45">Accent tone</p>
|
||||
<div class="mt-3 flex flex-wrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-full border px-4 py-2 text-sm transition hover:-translate-y-0.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-signal/35"
|
||||
:class="
|
||||
tone === 'signal'
|
||||
? 'border-signal/40 bg-signal/12 text-ink'
|
||||
: 'border-ink/10 bg-white text-ink/75'
|
||||
"
|
||||
@click="tone = 'signal'"
|
||||
>
|
||||
Signal accent
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-full border px-4 py-2 text-sm transition hover:-translate-y-0.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-clay/35"
|
||||
:class="
|
||||
tone === 'clay'
|
||||
? 'border-clay/40 bg-clay/16 text-ink'
|
||||
: 'border-ink/10 bg-white text-ink/75'
|
||||
"
|
||||
@click="tone = 'clay'"
|
||||
>
|
||||
Clay accent
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-[24px] border border-ink/10 bg-ink/5 p-4">
|
||||
<p class="text-xs uppercase tracking-[0.28em] text-ink/45">Density</p>
|
||||
<div class="mt-3 flex flex-wrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-full border px-4 py-2 text-sm transition hover:-translate-y-0.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-signal/35"
|
||||
:class="
|
||||
density === 'comfortable'
|
||||
? 'border-ink bg-ink text-paper'
|
||||
: 'border-ink/10 bg-white text-ink/75'
|
||||
"
|
||||
@click="density = 'comfortable'"
|
||||
>
|
||||
Comfortable
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-full border px-4 py-2 text-sm transition hover:-translate-y-0.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-signal/35"
|
||||
:class="
|
||||
density === 'compact'
|
||||
? 'border-ink bg-ink text-paper'
|
||||
: 'border-ink/10 bg-white text-ink/75'
|
||||
"
|
||||
@click="density = 'compact'"
|
||||
>
|
||||
Compact
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label class="rounded-[24px] border border-ink/10 bg-ink/5 p-4">
|
||||
<p class="text-xs uppercase tracking-[0.28em] text-ink/45">Overlay</p>
|
||||
<div class="mt-3 flex items-center justify-between gap-4">
|
||||
<span class="text-sm text-ink/75">Highlight overlay</span>
|
||||
<span class="relative inline-flex items-center">
|
||||
<input v-model="showOverlay" type="checkbox" class="peer sr-only" />
|
||||
<span
|
||||
class="h-6 w-11 rounded-full bg-ink/15 transition peer-checked:bg-signal/35"
|
||||
/>
|
||||
<span
|
||||
class="pointer-events-none absolute left-1 top-1 size-4 rounded-full bg-paper shadow-sm transition peer-checked:translate-x-5"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
:class="previewSurfaceClass"
|
||||
class="group relative overflow-hidden rounded-[32px] border border-paper/14 p-1 shadow-panel"
|
||||
>
|
||||
<div
|
||||
v-if="showOverlay"
|
||||
class="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_top_right,rgba(129,199,229,0.26),transparent_36%),radial-gradient(circle_at_bottom_left,rgba(203,153,112,0.16),transparent_34%)]"
|
||||
/>
|
||||
|
||||
<div
|
||||
:class="framePaddingClass"
|
||||
class="relative grid h-full rounded-[28px] border border-ink/8 bg-paper/86 backdrop-blur-sm"
|
||||
>
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<span
|
||||
class="rounded-full border px-3 py-1 text-[11px] uppercase tracking-[0.28em]"
|
||||
:class="
|
||||
tone === 'signal'
|
||||
? 'border-signal/30 bg-signal/12 text-ink'
|
||||
: 'border-clay/40 bg-clay/16 text-ink'
|
||||
"
|
||||
>
|
||||
{{ tone === 'signal' ? 'signal token' : 'clay token' }}
|
||||
</span>
|
||||
<span class="text-xs uppercase tracking-[0.28em] text-ink/45">
|
||||
{{ densityLabel }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 md:grid-cols-[1.45fr_0.55fr]">
|
||||
<div class="space-y-3">
|
||||
<h4 class="font-display text-3xl leading-none sm:text-4xl">
|
||||
Utilities should feel obvious.
|
||||
</h4>
|
||||
<p class="max-w-xl text-sm leading-7 text-ink/70">
|
||||
这块同时验证自定义主题 token、任意值圆角、阴影、响应式布局和状态类是否全部生效。
|
||||
</p>
|
||||
|
||||
<div class="space-y-3 rounded-[24px] border border-ink/10 bg-white/65 p-4">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<span class="text-xs uppercase tracking-[0.28em] text-ink/45">Build signal</span>
|
||||
<span
|
||||
class="rounded-full bg-ink px-3 py-1 text-xs uppercase tracking-[0.24em] text-paper"
|
||||
>
|
||||
ok
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="h-3 overflow-hidden rounded-full bg-ink/10">
|
||||
<div
|
||||
class="h-full rounded-full bg-ink transition-all duration-500"
|
||||
:style="{ width: `${progressValue}%` }"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<span class="rounded-full bg-ink/6 px-3 py-1 text-xs">rounded-[32px]</span>
|
||||
<span class="rounded-full bg-ink/6 px-3 py-1 text-xs">shadow-panel</span>
|
||||
<span class="rounded-full bg-ink/6 px-3 py-1 text-xs">sm:grid-cols-2</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3">
|
||||
<div
|
||||
class="rounded-[24px] border border-ink/10 bg-ink/5 p-4 transition duration-300 group-hover:-translate-y-1"
|
||||
>
|
||||
<p class="text-xs uppercase tracking-[0.28em] text-ink/45">Hover probe</p>
|
||||
<p class="mt-3 font-display text-2xl leading-none">group-hover</p>
|
||||
<p class="mt-2 text-sm leading-6 text-ink/70">
|
||||
把鼠标移到整张卡上,这块应当轻微上浮。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="rounded-[24px] border border-ink/10 bg-ink/5 p-4">
|
||||
<p class="text-xs uppercase tracking-[0.28em] text-ink/45">Focus probe</p>
|
||||
<button
|
||||
type="button"
|
||||
class="mt-3 rounded-full border border-ink/10 bg-white px-4 py-2 text-sm text-ink transition hover:-translate-y-0.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-signal/35"
|
||||
>
|
||||
Press Tab to focus
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="grid gap-5 lg:grid-cols-[minmax(0,0.95fr)_minmax(0,1.05fr)]">
|
||||
<div
|
||||
class="rounded-[32px] border border-paper/14 bg-paper/92 p-6 text-ink shadow-panel sm:p-7"
|
||||
>
|
||||
<p class="text-xs uppercase tracking-[0.32em] text-ink/45">Utility probes</p>
|
||||
<div class="mt-4 grid gap-3 md:grid-cols-3">
|
||||
<div
|
||||
v-for="probe in probeCards"
|
||||
:key="probe.title"
|
||||
class="rounded-[24px] border border-ink/10 bg-ink/5 p-4"
|
||||
>
|
||||
<p class="font-display text-2xl leading-none">{{ probe.title }}</p>
|
||||
<p
|
||||
class="mt-3 rounded-full bg-white px-3 py-1 text-xs uppercase tracking-[0.24em] text-ink/60"
|
||||
>
|
||||
{{ probe.detail }}
|
||||
</p>
|
||||
<p class="mt-4 text-sm leading-7 text-ink/70">{{ probe.note }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 rounded-[28px] border border-ink/10 bg-ink/5 p-4">
|
||||
<p class="text-xs uppercase tracking-[0.28em] text-ink/45">Responsive probes</p>
|
||||
<div class="mt-4 flex flex-wrap gap-2">
|
||||
<span
|
||||
class="rounded-full bg-ink px-3 py-1 text-xs uppercase tracking-[0.24em] text-paper"
|
||||
>
|
||||
base
|
||||
</span>
|
||||
<span
|
||||
class="hidden rounded-full bg-signal px-3 py-1 text-xs uppercase tracking-[0.24em] text-ink sm:inline-flex"
|
||||
>
|
||||
sm
|
||||
</span>
|
||||
<span
|
||||
class="hidden rounded-full bg-clay px-3 py-1 text-xs uppercase tracking-[0.24em] text-ink md:inline-flex"
|
||||
>
|
||||
md
|
||||
</span>
|
||||
<span
|
||||
class="hidden rounded-full bg-ink px-3 py-1 text-xs uppercase tracking-[0.24em] text-paper lg:inline-flex"
|
||||
>
|
||||
lg
|
||||
</span>
|
||||
<span
|
||||
class="hidden rounded-full border border-ink/15 bg-white px-3 py-1 text-xs uppercase tracking-[0.24em] text-ink xl:inline-flex"
|
||||
>
|
||||
xl
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 grid gap-3 md:grid-cols-2 xl:grid-cols-4">
|
||||
<div
|
||||
v-for="probe in responsiveProbes"
|
||||
:key="probe.label"
|
||||
class="rounded-[22px] border border-ink/10 bg-white/70 p-4"
|
||||
>
|
||||
<p class="font-display text-xl leading-none">{{ probe.label }}</p>
|
||||
<p class="mt-3 text-xs uppercase tracking-[0.24em] text-ink/45">{{ probe.hint }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="mt-4 text-sm leading-7 text-ink/70">
|
||||
拖动窗口时,上面的断点徽标会逐级出现,下面的卡片布局也会从 1 列切到 2 列和 4 列。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="rounded-[32px] border border-paper/14 bg-paper/92 p-6 text-ink shadow-panel sm:p-7"
|
||||
>
|
||||
<p class="text-xs uppercase tracking-[0.32em] text-ink/45">Checklists</p>
|
||||
<div class="mt-4 grid gap-4 xl:grid-cols-2">
|
||||
<div class="rounded-[24px] border border-ink/10 bg-ink/5 p-4">
|
||||
<p class="font-display text-2xl leading-none">Required checks</p>
|
||||
<ul class="mt-4 space-y-3">
|
||||
<li
|
||||
v-for="item in page.checks"
|
||||
:key="item"
|
||||
class="flex items-start gap-3 text-sm leading-7 text-ink/75"
|
||||
>
|
||||
<span class="mt-2 size-2 shrink-0 rounded-full bg-signal" />
|
||||
<span>{{ item }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="rounded-[24px] border border-ink/10 bg-ink/5 p-4">
|
||||
<p class="font-display text-2xl leading-none">Manual checks</p>
|
||||
<ul class="mt-4 space-y-3">
|
||||
<li
|
||||
v-for="item in page.manualChecks"
|
||||
:key="item"
|
||||
class="flex items-start gap-3 text-sm leading-7 text-ink/75"
|
||||
>
|
||||
<span class="mt-2 size-2 shrink-0 rounded-full bg-clay" />
|
||||
<span>{{ item }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="mt-6 rounded-[24px] border border-ink/10 bg-[linear-gradient(135deg,rgba(17,24,39,0.05),rgba(199,145,108,0.16))] p-5"
|
||||
>
|
||||
<p class="text-xs uppercase tracking-[0.28em] text-ink/45">Success criteria</p>
|
||||
<p class="mt-3 max-w-2xl font-display text-3xl leading-none">
|
||||
页面有证据,不是只有说明。
|
||||
</p>
|
||||
<p class="mt-3 max-w-2xl text-sm leading-7 text-ink/70">
|
||||
如果颜色、字型、交互和响应式都能在这个页面里被快速观察到,后面给任何新功能补最小验证页都会稳定很多。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
</template>
|
||||
@@ -1,46 +0,0 @@
|
||||
import type { Component } from 'vue'
|
||||
|
||||
import TailwindV4TestPage from '@/features/test-lab/pages/TailwindV4TestPage.vue'
|
||||
|
||||
export type FeatureTestPageStatus = 'ready' | 'draft'
|
||||
|
||||
export interface FeatureTestPage {
|
||||
slug: string
|
||||
title: string
|
||||
summary: string
|
||||
goal: string
|
||||
checks: string[]
|
||||
manualChecks: string[]
|
||||
status: FeatureTestPageStatus
|
||||
component: Component
|
||||
}
|
||||
|
||||
export const featureTestPages: FeatureTestPage[] = [
|
||||
{
|
||||
slug: 'tailwind-v4',
|
||||
title: 'Tailwind CSS v4',
|
||||
summary: 'Validate tokens, utilities, states, and responsive behavior in a single page.',
|
||||
goal: 'Confirm Tailwind CSS v4 is wired through Vite and available inside Vue SFCs.',
|
||||
checks: [
|
||||
'Custom theme tokens such as `font-display`, `text-signal`, and `shadow-panel` should render.',
|
||||
'Interactive states including `hover`, `focus-visible`, `peer-checked`, and `group-hover` should react.',
|
||||
'Responsive utilities should reveal extra badges and change the card grid as the viewport grows.',
|
||||
'Arbitrary values such as `rounded-[32px]` and `bg-[linear-gradient(...)]` should compile without missing styles.',
|
||||
],
|
||||
manualChecks: [
|
||||
'Click the tone and density controls to verify stateful class switches update immediately.',
|
||||
'Toggle the overlay switch and confirm the preview chrome appears and disappears without layout shifts.',
|
||||
'Resize the window across `sm`, `md`, `lg`, and `xl` widths to observe the breakpoint badges and grid changes.',
|
||||
],
|
||||
status: 'ready',
|
||||
component: TailwindV4TestPage,
|
||||
},
|
||||
]
|
||||
|
||||
const firstFeatureTestPage = featureTestPages[0]
|
||||
|
||||
if (!firstFeatureTestPage) {
|
||||
throw new Error('featureTestPages must contain at least one page')
|
||||
}
|
||||
|
||||
export const defaultFeatureTestPage: FeatureTestPage = firstFeatureTestPage
|
||||
@@ -1,14 +1,6 @@
|
||||
import { createRouter, createWebHistory, type RouterHistory } from 'vue-router'
|
||||
|
||||
import HomePage from '@/features/home/pages/HomePage.vue'
|
||||
import { defaultFeatureTestPage, featureTestPages } from '@/features/test-lab/registry'
|
||||
|
||||
const featureTestRoutes = featureTestPages.map((page) => ({
|
||||
path: `/lab/${page.slug}`,
|
||||
name: page.slug,
|
||||
component: page.component,
|
||||
meta: { page },
|
||||
}))
|
||||
|
||||
export function createAppRouter(
|
||||
history: RouterHistory = createWebHistory(import.meta.env.BASE_URL),
|
||||
@@ -21,11 +13,6 @@ export function createAppRouter(
|
||||
name: 'home',
|
||||
component: HomePage,
|
||||
},
|
||||
{
|
||||
path: '/lab',
|
||||
redirect: { name: defaultFeatureTestPage.slug },
|
||||
},
|
||||
...featureTestRoutes,
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
redirect: { name: 'home' },
|
||||
|
||||
Reference in New Issue
Block a user