ui4

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Payload UI Reskin (ui4)

Payload UI 换肤 (ui4)

Figma URL is REQUIRED. If not provided, ask before proceeding.

必须提供 Figma URL。 如果未提供,请在继续之前询问。

Process

流程

Step 0: Icon Scan

步骤 0:图标扫描

Goal: Identify icon dependencies before starting work.
  1. Scan component files for icon imports:
    grep -E "from.*icons|import.*Icon" packages/ui/src/elements/ComponentName/
  2. List existing icons in
    packages/ui/src/icons/
    :
    • Each icon has its own folder with
      index.tsx
      +
      index.css
  3. Compare Figma design to available icons:
    • Does the design use icons not currently in the component?
    • Does the design use icons that don't exist yet?
  4. Document findings:
    • Existing & used: No action needed
    • Existing but not imported: Will need to add import
    • Missing from codebase: Flag for user — need to source/create icon
Figma Icons Source:
When updating or creating icons, reference the Figma icon library at:
~/figma/figma/fpl/icons/src/icons/
Icon naming convention:
icon-{size}-{name}.tsx
(e.g.,
icon-16-close.tsx
,
icon-24-chevron-down.tsx
)
To find the correct icon:
  1. Note the icon name from Figma design (e.g., "close", "chevron-down")
  2. Check both 16px and 24px variants if they exist
  3. Read the corresponding files and extract the SVG paths for each size
Icon implementation rules:
  1. Props: Icon components MUST accept these props (keep existing props when updating):
    typescript
    type IconProps = {
      readonly className?: string
      readonly size?: 16 | 24 // Add more sizes as needed
      // ... keep any existing component-specific props
    }
  2. Multi-size support: Store path data keyed by size:
    typescript
    const paths = {
      16: 'M4.854 4.146...', // from icon-16-{name}.tsx
      24: 'M6.854 6.146...', // from icon-24-{name}.tsx
    }
  3. SVG rendering: Use the size prop to select path and viewBox:
    tsx
    <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} fill="none">
      <path d={paths[size]} fill="currentColor" />
    </svg>
  4. Payload conventions:
    • Use
      fill="currentColor"
      instead of
      fill="var(--color-icon)"
    • Use
      fillRule
      and
      clipRule
      (React camelCase) instead of kebab-case
    • Default size should match most common usage (typically 24)
  5. Reference implementation: See
    packages/ui/src/icons/Chevron/index.tsx
    for the pattern.
If icons are missing from Figma source: Ask user how to proceed before continuing.

目标: 在开始工作前识别图标依赖项。
  1. 扫描组件文件中的图标导入项:
    grep -E "from.*icons|import.*Icon" packages/ui/src/elements/ComponentName/
  2. 列出
    packages/ui/src/icons/
    中的现有图标:
    • 每个图标都有独立文件夹,其中包含
      index.tsx
      +
      index.css
  3. 将 Figma 设计与可用图标进行比较:
    • 设计是否使用了组件中目前没有的图标?
    • 设计是否使用了尚不存在的图标?
  4. 记录发现:
    • 已存在且正在使用: 无需操作
    • 已存在但未导入: 需要添加 import
    • 代码库中缺失: 标记给用户 — 需要获取或创建图标
Figma 图标源:
更新或创建图标时,参考以下 Figma 图标库:
~/figma/figma/fpl/icons/src/icons/
图标命名约定:
icon-{size}-{name}.tsx
(例如
icon-16-close.tsx
、
icon-24-chevron-down.tsx
)
查找正确图标的方法:
  1. 记下 Figma 设计中的图标名称(例如
    close
    、
    chevron-down
    )
  2. 如果 16px 和 24px 变体都存在,则检查两者
  3. 读取对应文件,并提取每种尺寸的 SVG path
图标实现规则:
  1. Props: 图标组件必须接受以下 props(更新时保留现有 props):
    typescript
    type IconProps = {
      readonly className?: string
      readonly size?: 16 | 24 // Add more sizes as needed
      // ... keep any existing component-specific props
    }
  2. 多尺寸支持: 按 size 存储 path 数据:
    typescript
    const paths = {
      16: 'M4.854 4.146...', // from icon-16-{name}.tsx
      24: 'M6.854 6.146...', // from icon-24-{name}.tsx
    }
  3. SVG 渲染: 使用 size prop 选择 path 和 viewBox:
    tsx
    <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} fill="none">
      <path d={paths[size]} fill="currentColor" />
    </svg>
  4. Payload 约定:
    • 使用
      fill="currentColor"
      ,而不是
      fill="var(--color-icon)"
    • 使用
      fillRule
      和
      clipRule
      (React camelCase),而不是 kebab-case
    • 默认 size 应匹配最常见的使用方式(通常为 24)
  5. 参考实现: 参阅
    packages/ui/src/icons/Chevron/index.tsx
    了解具体模式。
如果 Figma 源中缺少图标: 在继续之前询问用户应如何处理。

Step 1: SCSS → CSS Migration

步骤 1:SCSS → CSS 迁移

Goal: Syntax conversion only. Component must look IDENTICAL after.
  1. Read component files:
    packages/ui/src/elements/ComponentName/
    or
    packages/ui/src/fields/ComponentName/
  2. Create
    index.css
    with converted styles:
    • $var
      →
      var(--token)
    • Keep CSS nesting with
      &
      (preferred)
    • Remove
      @use
      /
      @import
      (tokens are global)
    • Inline any mixins
  3. Update import:
    import './index.scss'
    →
    import './index.css'
  4. Delete
    index.scss
  5. Wrap in
    @layer payload-default {}
  6. Convert legacy
    var(--base)
    to
    --spacer
    tokens
    (see below)
  7. Check for SCSS-only variables (see below)

目标: 仅转换语法。迁移后组件外观必须完全一致。
  1. 读取组件文件:
    packages/ui/src/elements/ComponentName/
    或
    packages/ui/src/fields/ComponentName/
  2. 使用转换后的样式创建
    index.css
    :
    • $var
      →
      var(--token)
    • 保留使用
      &
      的 CSS 嵌套(推荐)
    • 移除
      @use
      /
      @import
      (令牌是全局的)
    • 内联所有 mixins
  3. 更新导入:
    import './index.scss'
    →
    import './index.css'
  4. 删除
    index.scss
  5. 包裹在
    @layer payload-default {}
    中
  6. 将旧版
    var(--base)
    转换为
    --spacer
    令牌
    (见下文)
  7. 检查仅存在于 SCSS 的变量(见下文)

SCSS Variable Dependencies

SCSS 变量依赖项

CRITICAL: The
packages/ui/src/scss/
folder has been removed. All global tokens now live in
packages/ui/src/css/
. Any CSS variable you use must exist there.
Before using a variable, verify it exists in the CSS folder:
bash
grep -r "variable-name" packages/ui/src/css/
If a variable is only in SCSS:
  1. Check if there's an equivalent in the CSS folder
  2. If not, add it to the appropriate CSS file:
    • spacing.css
      — spacers, gutters, layout spacing, breakpoints
    • colors.css
      — color tokens
    • typography.css
      — font tokens
    • radius.css
      — border-radius tokens
    • utilities.css
      — accessibility, misc utilities
Common SCSS-only variables to watch for:
SCSS VariableCSS Equivalent / Action
--spacing-view-bottom
Defined in
spacing.css
--breakpoint-m-width
Defined in
spacing.css
(1024px)
--breakpoint-s-width
Defined in
spacing.css
(768px)
--gutter-h
Defined in
spacing.css
$breakpoint-m-width
Use
var(--breakpoint-m-width)
in media queries
@include mid-break
Use
@media (max-width: 1024px)
@include small-break
Use
@media (max-width: 768px)

关键:
packages/ui/src/scss/
文件夹已删除。所有全局令牌现在位于
packages/ui/src/css/
中。使用的每个 CSS 变量都必须存在于该目录中。
使用变量前,验证它是否存在于 CSS 文件夹中:
bash
grep -r "variable-name" packages/ui/src/css/
如果变量仅存在于 SCSS 中:
  1. 检查 CSS 文件夹中是否有等效变量
  2. 如果没有,将其添加到适当的 CSS 文件中:
    • spacing.css
      — 间距、边距、布局间距和断点
    • colors.css
      — 颜色令牌
    • typography.css
      — 字体令牌
    • radius.css
      — 圆角令牌
    • utilities.css
      — 无障碍和其他实用工具
需要关注的常见 SCSS 专属变量:
SCSS 变量CSS 等效项/操作
--spacing-view-bottom定义于
spacing.css
--breakpoint-m-width定义于
spacing.css
(1024px)
--breakpoint-s-width定义于
spacing.css
(768px)
--gutter-h定义于
spacing.css
$breakpoint-m-width在媒体查询中使用
var(--breakpoint-m-width)
@include mid-break使用
@media (max-width: 1024px)
@include small-break使用
@media (max-width: 768px)

Legacy Token Migration:
var(--base)
→
--spacer

旧版令牌迁移:
var(--base)
→
--spacer

What is
--base
?
A legacy spacing token equal to
20px
(1.25rem). It must be replaced with
--spacer-*
tokens.
Spacer token values:
TokenValuePixels
--spacer-0
00px
--spacer-1
4px4px
--spacer-2
8px8px
--spacer-2-5
12px12px
--spacer-3
16px16px
--spacer-4
24px24px
--spacer-5
32px32px
--spacer-6
40px40px
Conversion strategy:
  1. Direct match: If the result equals a spacer token, use it directly:
    css
    /* Before: var(--base) = 20px → closest is --spacer-3 (16px) or --spacer-4 (24px) */
    padding: var(--base);
    
    /* After: Choose semantically correct size */
    padding: var(--spacer-4); /* if 24px is acceptable */
  2. Calculated values: When exact pixel value is important, use
    calc()
    :
    css
    /* Before: calc(var(--base) * 0.5) = 10px */
    gap: calc(var(--base) * 0.5);
    
    /* After: calc(var(--spacer-1) * 2.5) = 10px */
    gap: calc(var(--spacer-1) * 2.5);
  3. ALWAYS round to nearest spacer token. Never use
    calc()
    to preserve non-standard pixel values. Round all calculated values to the nearest token:
    Pixel RangeTokenNotes
    0-2px
    --spacer-0
    Use 0
    3-6px
    --spacer-1
    (4px)
    5-6px rounds to 4px
    7-10px
    --spacer-2
    (8px)
    10px rounds DOWN to 8px
    11-14px
    --spacer-2-5
    (12px)
    13.33px rounds to 12px
    15-20px
    --spacer-3
    (16px)
    15px, 20px both round to 16px
    21-28px
    --spacer-4
    (24px)
    29-36px
    --spacer-5
    (32px)
    30px rounds to 32px
    37-48px
    --spacer-6
    (40px)
  4. Common
    var(--base)
    conversions
    (base = 20px):
    OriginalPixelsRounded Token
    var(--base) * 0.25
    5px
    --spacer-1
    (4px)
    var(--base) * 0.3
    6px
    --spacer-1
    (4px)
    var(--base) * 0.4
    8px
    --spacer-2
    var(--base) * 0.5
    10px
    --spacer-2
    (8px)
    var(--base) * 0.6
    12px
    --spacer-2-5
    var(--base) / 1.5
    13.3px
    --spacer-2-5
    (12px)
    var(--base) * 0.75
    15px
    --spacer-3
    (16px)
    var(--base) * 0.8
    16px
    --spacer-3
    var(--base)
    20px
    --spacer-3
    (16px) or
    --spacer-4
    (24px)
    var(--base) * 1.2
    24px
    --spacer-4
    var(--base) * 1.5
    30px
    --spacer-5
    (32px)
    var(--base) * 2
    40px
    --spacer-6
    var(--base) * 3
    60px
    calc(var(--spacer-4) * 2.5)
    — only use calc for values > 40px
    Rule: For values ≤ 40px, ALWAYS use a single token. For values > 40px, use
    calc()
    with a spacer token.
  5. Check Figma design: The best approach is to check the Figma design for the intended spacing value and use the matching
    --spacer-*
    token directly.

CRITICAL: SCSS nesting patterns that DON'T work in CSS:
1. BEM element concatenation (
&__element
):
scss
// SCSS - WORKS (produces .block__element)
.block {
  &__element {
    color: red;
  }
  &__other {
    color: blue;
  }
}
css
/* CSS - DOES NOT WORK! &__element is invalid */
/* You must use flat selectors: */
.block { ... }
.block__element { color: red; }
.block__other { color: blue; }
2. BEM modifier concatenation (
&--modifier
):
scss
// SCSS - WORKS (produces .block--active)
.block {
  &--active {
    background: blue;
  }
}
css
/* CSS - DOES NOT WORK! Use flat selector: */
.block { ... }
.block--active { background: blue; }
3. Parent reference from child:
scss
// SCSS - WORKS
.child {
  opacity: 0.5;
  .parent--active & {
    opacity: 1;
  }
}
css
/* CSS - DOES NOT WORK! Restructure: */
.child {
  opacity: 0.5;
}
.parent--active .child {
  opacity: 1;
}
What DOES work in CSS nesting:
  • &:hover
    ,
    &:focus
    ,
    &:active
    (pseudo-classes)
  • &::before
    ,
    &::after
    (pseudo-elements)
  • .parent { .child { } }
    (descendant nesting with space)
Migration rule: Convert all
&__
and
&--
to flat BEM selectors.
Post-migration validation: After creating the CSS file, run the ui4-review skill to catch any remaining violations (SCSS nesting patterns, hardcoded values, legacy variables). Fix any issues before proceeding.
什么是
--base
?
一个等于
20px
(
1.25rem
)的旧版间距令牌。必须将其替换为
--spacer-*
令牌。
间距令牌值:
令牌数值像素
--spacer-0
00px
--spacer-1
4px4px
--spacer-2
8px8px
--spacer-2-5
12px12px
--spacer-3
16px16px
--spacer-4
24px24px
--spacer-5
32px32px
--spacer-6
40px40px
转换策略:
  1. 直接匹配: 如果结果与某个间距令牌相等,直接使用:
    css
    /* Before: var(--base) = 20px → closest is --spacer-3 (16px) or --spacer-4 (24px) */
    padding: var(--base);
    
    /* After: Choose semantically correct size */
    padding: var(--spacer-4); /* if 24px is acceptable */
  2. 计算值: 当精确像素值很重要时,使用
    calc()
    :
    css
    /* Before: calc(var(--base) * 0.5) = 10px */
    gap: calc(var(--base) * 0.5);
    
    /* After: calc(var(--spacer-1) * 2.5) = 10px */
    gap: calc(var(--spacer-1) * 2.5);
  3. 始终舍入到最接近的间距令牌。 绝不要使用
    calc()
    保留非标准像素值。将所有计算值舍入到最接近的令牌:
    像素范围令牌说明
    0-2px
    --spacer-0
    使用 0
    3-6px
    --spacer-1
    (4px)
    5-6px 舍入为 4px
    7-10px
    --spacer-2
    (8px)
    10px 向下舍入为 8px
    11-14px
    --spacer-2-5
    (12px)
    13.33px 舍入为 12px
    15-20px
    --spacer-3
    (16px)
    15px、20px 均舍入为 16px
    21-28px
    --spacer-4
    (24px)
    29-36px
    --spacer-5
    (32px)
    30px 舍入为 32px
    37-48px
    --spacer-6
    (40px)
  4. 常见的
    var(--base)
    转换
    (base = 20px):
    原始值像素舍入后的令牌
    var(--base) * 0.25
    5px
    --spacer-1
    (4px)
    var(--base) * 0.3
    6px
    --spacer-1
    (4px)
    var(--base) * 0.4
    8px
    --spacer-2
    var(--base) * 0.5
    10px
    --spacer-2
    (8px)
    var(--base) * 0.6
    12px
    --spacer-2-5
    var(--base) / 1.5
    13.3px
    --spacer-2-5
    (12px)
    var(--base) * 0.75
    15px
    --spacer-3
    (16px)
    var(--base) * 0.8
    16px
    --spacer-3
    var(--base)
    20px
    --spacer-3
    (16px)或
    --spacer-4
    (24px)
    var(--base) * 1.2
    24px
    --spacer-4
    var(--base) * 1.5
    30px
    --spacer-5
    (32px)
    var(--base) * 2
    40px
    --spacer-6
    var(--base) * 3
    60px
    calc(var(--spacer-4) * 2.5)
    — 仅当值大于 40px 时使用 calc
    规则: 对于小于或等于 40px 的值,始终使用单个令牌。对于大于 40px 的值,使用
    calc()
    配合间距令牌。
  5. 检查 Figma 设计: 最佳做法是检查 Figma 设计中的预期间距值,并直接使用匹配的
    --spacer-*
    令牌。

关键:不适用于 CSS 的 SCSS 嵌套模式:
1. BEM 元素拼接(
&__element
):
scss
// SCSS - WORKS (produces .block__element)
.block {
  &__element {
    color: red;
  }
  &__other {
    color: blue;
  }
}
css
/* CSS - DOES NOT WORK! &__element is invalid */
/* You must use flat selectors: */
.block { ... }
.block__element { color: red; }
.block__other { color: blue; }
2. BEM 修饰符拼接(
&--modifier
):
scss
// SCSS - WORKS (produces .block--active)
.block {
  &--active {
    background: blue;
  }
}
css
/* CSS - DOES NOT WORK! Use flat selector: */
.block { ... }
.block--active { background: blue; }
3. 从子元素引用父元素:
scss
// SCSS - WORKS
.child {
  opacity: 0.5;
  .parent--active & {
    opacity: 1;
  }
}
css
/* CSS - DOES NOT WORK! Restructure: */
.child {
  opacity: 0.5;
}
.parent--active .child {
  opacity: 1;
}
CSS 嵌套中可用的模式:
  • &:hover
    、
    &:focus
    、
    &:active
    (伪类)
  • &::before
    、
    &::after
    (伪元素)
  • .parent { .child { } }
    (带空格的后代嵌套)
迁移规则: 将所有
&__
和
&--
转换为扁平的 BEM 选择器。
迁移后验证: 创建 CSS 文件后,运行 ui4-review skill 以发现任何剩余违规(SCSS 嵌套模式、硬编码值、旧版变量)。继续之前修复所有问题。

Step 2: Analyze Figma Component Variants

步骤 2:分析 Figma 组件变体

Goal: Understand ALL visual states before implementing CSS.
  1. Get metadata first to discover variants:
    mcp_figma2_get_metadata(fileKey, nodeId)
  2. Parse variant properties from symbol names. Common patterns:
    • State=Default, Validation=None, Selected=false, Read Only=false
    • Properties often include: State, Validation, Selected, Disabled, Read Only, Size
  3. Build a variant matrix:
    StateValidationSelectedRead OnlyCSS Mapping
    DefaultNonefalsefalsebase styles
    HoverNonefalsefalse
    :hover
    FocusNonefalsefalse
    :focus-visible
    DefaultInvalidfalsefalse
    .error
    or
    &--error
    DefaultNonetruefalse
    .is-selected
    DefaultNonefalsetrue
    .read-only
    ,
    [disabled]
  4. Fetch design context for key variants (in parallel):
    • Default unselected
    • Selected
    • Hover
    • Focus
    • Invalid/Error
    • Disabled/Read-only
    mcp_figma2_get_design_context(fileKey, variantNodeId)
  5. Compare visual differences between variants:
    • Border color changes?
    • Background color changes?
    • Inner element visibility/opacity?
    • Focus ring/outline?
    • Text color changes?
If access fails: STOP. Ask user to share file.
目标: 实现 CSS 前了解所有视觉状态。
  1. 先获取元数据以发现变体:
    mcp_figma2_get_metadata(fileKey, nodeId)
  2. 从符号名称中解析变体属性。 常见模式:
    • State=Default, Validation=None, Selected=false, Read Only=false
    • 属性通常包括:State、Validation、Selected、Disabled、Read Only、Size
  3. 构建变体矩阵:
    StateValidationSelectedRead OnlyCSS 映射
    DefaultNonefalsefalse基础样式
    HoverNonefalsefalse
    :hover
    FocusNonefalsefalse
    :focus-visible
    DefaultInvalidfalsefalse
    .error
    或
    &--error
    DefaultNonetruefalse
    .is-selected
    DefaultNonefalsetrue
    .read-only
    、
    [disabled]
  4. 并行获取关键变体的设计上下文:
    • 默认未选中
    • 已选中
    • 悬停
    • 聚焦
    • 无效/错误
    • 禁用/只读
    mcp_figma2_get_design_context(fileKey, variantNodeId)
  5. 比较变体之间的视觉差异:
    • 边框颜色是否变化?
    • 背景颜色是否变化?
    • 内部元素的可见性/透明度是否变化?
    • 聚焦环/轮廓是否变化?
    • 文字颜色是否变化?
如果访问失败: 停止操作。请用户提供文件。

Step 3: Restyle to Match Figma

步骤 3:重新设计样式以匹配 Figma

  1. Read token files (do this BEFORE writing CSS):
    • packages/ui/src/css/spacing.css
      — spacer tokens
    • packages/ui/src/css/colors.css
      — color tokens
    • packages/ui/src/css/typography.css
      — text tokens
    • packages/ui/src/css/radius.css
      — border-radius tokens
    • packages/ui/src/css/utilities.css
      — accessibility tokens
  2. Update styles using tokens from files:
    • Colors:
      --bg-*
      ,
      --text-*
      ,
      --icon-*
      ,
      --border-*
    • Spacing:
      --spacer-*
      (ALWAYS check file for matching value)
    • Typography:
      --text-body-*
      ,
      --text-heading-*
    • Radius:
      --radius-none/small/medium/large/full
    • Focus states:
      --accessibility-focus-color
      (NEVER use
      --color-border-selected
      directly)
  3. Focus state rules:
    • Always use
      --accessibility-focus-color
      for focus outlines/borders
    • Use
      :focus-visible
      (not
      :focus
      ) for keyboard-only focus
    • Standard focus outline:
      outline: 1px solid var(--accessibility-focus-color)
    • For parent containers: use
      :has(:focus-visible)
      to detect child focus
  4. Use canonical shorthands — see the shorthand table in
    .agents/skills/ui4-review/SKILL.md
    .
  5. Color rules — NEVER GUESS:
    • Always extract exact token from Figma design context — the
      get_design_context
      response includes CSS with token names
    • Don't assume hierarchy — e.g., don't assume "less prominent = tertiary". Check the design.
    • When creating new elements (icons, buttons, etc.), fetch the specific Figma node to get correct colors
    • If Figma shows a raw hex value, map it to the closest token and note this for user review
  6. Spacing rules:
    • First choice: use
      --spacer-*
      token
    • If no match: use rem and tell user
    • NEVER use px (except 1px borders)
  1. 读取令牌文件(必须在编写 CSS 前完成):
    • packages/ui/src/css/spacing.css
      — 间距令牌
    • packages/ui/src/css/colors.css
      — 颜色令牌
    • packages/ui/src/css/typography.css
      — 文字令牌
    • packages/ui/src/css/radius.css
      — 圆角令牌
    • packages/ui/src/css/utilities.css
      — 无障碍令牌
  2. 使用文件中的令牌更新样式:
    • 颜色:
      --bg-*
      、
      --text-*
      、
      --icon-*
      、
      --border-*
    • 间距:
      --spacer-*
      (始终检查文件中是否存在匹配值)
    • 字体:
      --text-body-*
      、
      --text-heading-*
    • 圆角:
      --radius-none/small/medium/large/full
    • 聚焦状态:
      --accessibility-focus-color
      (绝不直接使用
      --color-border-selected
      )
  3. 聚焦状态规则:
    • 始终将
      --accessibility-focus-color
      用于聚焦轮廓/边框
    • 使用
      :focus-visible
      (而非
      :focus
      )实现仅键盘聚焦
    • 标准聚焦轮廓:
      outline: 1px solid var(--accessibility-focus-color)
    • 对于父容器,使用
      :has(:focus-visible)
      检测子元素焦点
  4. 使用标准简写 — 参阅
    .agents/skills/ui4-review/SKILL.md
    中的简写表。
  5. 颜色规则 — 绝不猜测:
    • 始终从 Figma 设计上下文中提取准确令牌 —
      get_design_context
      响应包含带有令牌名称的 CSS
    • 不要假设层级 — 例如,不要假设“弱化 = tertiary”。请检查设计。
    • 创建新元素时(图标、按钮等),获取对应的 Figma 节点以获取正确颜色
    • 如果 Figma 显示原始十六进制值,将其映射到最接近的令牌,并告知用户以便审核
  6. 间距规则:
    • 首选
      --spacer-*
      令牌
    • 如果没有匹配项,使用 rem 并告知用户
    • 绝不使用 px(1px 边框除外)

Step 4: Ensure Test Collection Has All Variants

步骤 4:确保测试 Collection 包含所有变体

Goal: Before visual verification, ensure the test collection has field variants for all states.
  1. Read the test collection config:
    test/v4/collections/{ComponentName}/index.ts
  2. Check for required variants based on Figma variant matrix from Step 2:
    Figma VariantRequired Field Config
    Default
    { name: 'default', type: 'component' }
    Required
    { name: 'required', type: 'component', required: true }
    Disabled
    { name: 'disabled', type: 'component', admin: { disabled: true }, defaultValue: '...' }
    Read Only
    { name: 'readOnly', type: 'component', admin: { readOnly: true }, defaultValue: '...' }
    With Description
    { name: 'withDescription', type: 'component', admin: { description: 'Help text' } }
  3. Add missing variants if any are missing. Include
    defaultValue
    for disabled/readOnly so there's visible content to test.
  4. Restart dev server if collection was modified:
    pnpm run dev v4

目标: 在进行视觉验证之前,确保测试 collection 包含所有字段状态变体。
  1. 读取测试 collection 配置:
    test/v4/collections/{ComponentName}/index.ts
  2. 根据步骤 2 中的 Figma 变体矩阵检查必需变体:
    Figma 变体必需字段配置
    Default
    { name: 'default', type: 'component' }
    Required
    { name: 'required', type: 'component', required: true }
    Disabled
    { name: 'disabled', type: 'component', admin: { disabled: true }, defaultValue: '...' }
    Read Only
    { name: 'readOnly', type: 'component', admin: { readOnly: true }, defaultValue: '...' }
    With Description
    { name: 'withDescription', type: 'component', admin: { description: 'Help text' } }
  3. 如果缺少任何变体,则添加。 为 disabled/readOnly 包含
    defaultValue
    ,以提供可见的测试内容。
  4. 如果修改了 collection,则重启开发服务器:
    pnpm run dev v4

Step 5: Verify with Playwright (LOOP)

步骤 5:使用 Playwright 验证(循环)

Dev Server: Use
pnpm run dev v4
when working on field components. The
test/v4
suite has dedicated collections for each field type with various states (default, required, disabled).
URL Pattern:
  • Fields:
    http://localhost:3000/admin/collections/{field-type}-fields/create
  • Elements: Use the appropriate page that displays the element
Handling Modal Dialogs (beforeunload):
When the browser has unsaved changes, a "beforeunload" dialog may block ALL Playwright operations. You'll see this error pattern:
undefined
开发服务器: 处理字段组件时,使用
pnpm run dev v4
。
test/v4
测试套件为每种字段类型提供专用 collection,并包含多种状态(default、required、disabled)。
URL 模式:
  • 字段:
    http://localhost:3000/admin/collections/{field-type}-fields/create
  • 元素:使用显示该元素的相应页面
处理模态对话框(beforeunload):
当浏览器存在未保存的更改时,
beforeunload
对话框可能会阻止所有 Playwright 操作。你会看到以下错误模式:
undefined

Error

Error

Error: Tool "browser_snapshot" does not handle the modal state.
Error: Tool "browser_snapshot" does not handle the modal state.

Modal state

Modal state

  • ["beforeunload" dialog with message ""]: can be handled by browser_handle_dialog

**BEFORE retrying any operation**, you MUST dismiss the dialog:

1. Call `browser_handle_dialog({ accept: true })` to dismiss
2. Then retry your intended operation (navigate, snapshot, screenshot, etc.)

If the dialog persists after handling, call `browser_close()` to close the tab, then `browser_navigate` to reopen the page fresh.

**Verification Steps:**

1. Navigate: `browser_navigate` to component page
2. Screenshot: `browser_take_screenshot({ fullPage: true })`
3. Compare to Figma design
4. Check:
   - [ ] Padding correct?
   - [ ] Margin correct?
   - [ ] Gap correct?
   - [ ] Flex alignment correct?
   - [ ] Colors match?
5. **Verify ALL variant states:**
   - [ ] Default state matches Figma default variant?
   - [ ] Hover state matches Figma hover variant? (use `browser_hover`)
   - [ ] Focus state matches Figma focus variant? (tab to element)
   - [ ] Error state matches Figma invalid variant? (trigger validation)
   - [ ] Disabled/read-only matches Figma disabled variant?
   - [ ] Selected state matches Figma selected variant? (if applicable)
6. **If wrong:** fix CSS → goto step 1
7. **If correct:** continue
  • ["beforeunload" dialog with message ""]: can be handled by browser_handle_dialog

**在重试任何操作之前**,必须关闭该对话框:

1. 调用 `browser_handle_dialog({ accept: true })` 关闭对话框
2. 然后重试预期操作(导航、快照、截图等)

如果处理后对话框仍然存在,调用 `browser_close()` 关闭标签页,然后调用 `browser_navigate` 重新打开全新页面。

**验证步骤:**

1. 导航:使用 `browser_navigate` 打开组件页面
2. 截图:`browser_take_screenshot({ fullPage: true })`
3. 与 Figma 设计进行比较
4. 检查:
   - [ ] Padding 是否正确?
   - [ ] Margin 是否正确?
   - [ ] Gap 是否正确?
   - [ ] Flex 对齐是否正确?
   - [ ] 颜色是否匹配?
5. **验证所有变体状态:**
   - [ ] Default 状态是否匹配 Figma default 变体?
   - [ ] Hover 状态是否匹配 Figma hover 变体?(使用 `browser_hover`)
   - [ ] Focus 状态是否匹配 Figma focus 变体?(Tab 切换到元素)
   - [ ] Error 状态是否匹配 Figma invalid 变体?(触发验证)
   - [ ] Disabled/read-only 是否匹配 Figma disabled 变体?
   - [ ] Selected 状态是否匹配 Figma selected 变体?(如适用)
6. **如果有误:** 修复 CSS → 转到步骤 1
7. **如果正确:** 继续

Step 6: User Confirmation

步骤 6:用户确认

Share screenshot and dev server URL. User validates or requests changes.

分享截图和开发服务器 URL。由用户进行验证或提出更改请求。

CSS Structure

CSS 结构

Always use
@layer
and CSS nesting:
css
@layer payload-default {
  .component {
    display: flex;
    gap: var(--spacer-2);
    padding: var(--spacer-2) var(--spacer-3);
    background: var(--bg-default-secondary);
    border: 1px solid var(--border-default-default);
    border-radius: var(--radius-medium);

    &__header {
      display: flex;
      gap: var(--spacer-1);
    }

    &--error {
      border-color: var(--border-danger-strong);
    }

    &:hover {
      background: var(--bg-default-secondary-hover);
    }
  }
}

始终使用
@layer
和 CSS 嵌套:
css
@layer payload-default {
  .component {
    display: flex;
    gap: var(--spacer-2);
    padding: var(--spacer-2) var(--spacer-3);
    background: var(--bg-default-secondary);
    border: 1px solid var(--border-default-default);
    border-radius: var(--radius-medium);

    &__header {
      display: flex;
      gap: var(--spacer-1);
    }

    &--error {
      border-color: var(--border-danger-strong);
    }

    &:hover {
      background: var(--bg-default-secondary-hover);
    }
  }
}

Red Flags - STOP

危险信号 — 停止

ThoughtReality
"I'll use flat selectors"Use CSS nesting with
&
"I'll use 8px here"Read spacing.css, use token
"No matching spacer"Did you actually check spacing.css?
"I'll guess the colors"Read colors.css, use exact token
"Close enough"Screenshot and compare to Figma
"Skip verification"Always run Playwright loop
"Just need the default state"Get metadata first, analyze ALL variants
"I'll figure out states later"Build variant matrix BEFORE writing CSS

想法实际情况
“我会使用扁平选择器”使用带
&
的 CSS 嵌套
“我会在这里使用 8px”读取 spacing.css,使用令牌
“没有匹配的 spacer”你真的检查 spacing.css 了吗?
“我会猜颜色”读取 colors.css,使用准确令牌
“足够接近了”截图并与 Figma 比较
“跳过验证”始终运行 Playwright 循环
“只需要 default 状态”先获取元数据,分析所有变体
“状态稍后再处理”编写 CSS 前先构建变体矩阵

Step 7: Write Variant E2E Tests

步骤 7:编写变体 E2E 测试

Goal: Create e2e tests that verify all visual variants from the Figma design.
  1. Create test file in
    test/v4/collections/{ComponentName}/e2e.spec.ts
  2. Test structure:
    typescript
    import type { Page } from '@playwright/test'
    import { expect, test } from '@playwright/test'
    import path from 'path'
    import { fileURLToPath } from 'url'
    
    import {
      ensureCompilationIsDone,
      initPageConsoleErrorCatch,
    } from '../../../__helpers/e2e/helpers.js'
    import { AdminUrlUtil } from '../../../__helpers/shared/adminUrlUtil.js'
    import { initPayloadE2ENoConfig } from '../../../__helpers/shared/initPayloadE2ENoConfig.js'
    import { TEST_TIMEOUT_LONG } from '../../../playwright.config.js'
    import { componentFieldsSlug } from '../../slugs.js'
    
    const filename = fileURLToPath(import.meta.url)
    const currentFolder = path.dirname(filename)
    const dirname = path.resolve(currentFolder, '../../')
    
    const { beforeAll, describe } = test
    
    let page: Page
    let serverURL: string
    let url: AdminUrlUtil
    
    describe('ComponentName Field Variants', () => {
      beforeAll(async ({ browser }, testInfo) => {
        testInfo.setTimeout(TEST_TIMEOUT_LONG)
        ;({ serverURL } = await initPayloadE2ENoConfig({ dirname }))
        url = new AdminUrlUtil(serverURL, componentFieldsSlug)
        const context = await browser.newContext()
        page = await context.newPage()
        initPageConsoleErrorCatch(page)
        await ensureCompilationIsDone({ page, serverURL })
      })
    
      // Test each variant from the Figma design
    })
  3. Write tests for each Figma variant:
    Map the variant matrix from Step 2 to test cases:
    typescript
    test('default state renders correctly', async () => {
      await page.goto(url.create)
      const field = page.locator('#field-componentName')
      await expect(field).toBeVisible()
      // Verify visual properties match Figma default variant
    })
    
    test('hover state shows correct styling', async () => {
      await page.goto(url.create)
      const field = page.locator('#field-componentName')
      await field.hover()
      // Verify hover styles match Figma hover variant
    })
    
    test('focus state shows correct styling', async () => {
      await page.goto(url.create)
      const field = page.locator('#field-componentName')
      await field.focus()
      // Verify focus ring/outline matches Figma focus variant
    })
    
    test('error state renders correctly', async () => {
      await page.goto(url.create)
      // Trigger validation by submitting without required field
      await page.locator('button#action-save').click()
      const field = page.locator('#field-requiredComponent')
      // Verify error styling matches Figma invalid variant
    })
    
    test('disabled state renders correctly', async () => {
      await page.goto(url.create)
      const field = page.locator('#field-disabledComponent')
      await expect(field).toBeDisabled()
      // Verify disabled styling matches Figma disabled variant
    })
    
    test('read-only state renders correctly', async () => {
      await page.goto(url.create)
      const field = page.locator('#field-readOnlyComponent')
      await expect(field).toHaveAttribute('readonly')
      // Verify read-only styling matches Figma read-only variant
    })
  4. Collection variants already configured: (See Step 4)
    The test collection should already have all required variants from Step 4.
  5. Run tests to verify:
    bash
    pnpm run test:e2e --grep "ComponentName Field Variants"

目标: 创建 e2e 测试,验证 Figma 设计中的所有视觉变体。
  1. 创建测试文件
    test/v4/collections/{ComponentName}/e2e.spec.ts
  2. 测试结构:
    typescript
    import type { Page } from '@playwright/test'
    import { expect, test } from '@playwright/test'
    import path from 'path'
    import { fileURLToPath } from 'url'
    
    import {
      ensureCompilationIsDone,
      initPageConsoleErrorCatch,
    } from '../../../__helpers/e2e/helpers.js'
    import { AdminUrlUtil } from '../../../__helpers/shared/adminUrlUtil.js'
    import { initPayloadE2ENoConfig } from '../../../__helpers/shared/initPayloadE2ENoConfig.js'
    import { TEST_TIMEOUT_LONG } from '../../../playwright.config.js'
    import { componentFieldsSlug } from '../../slugs.js'
    
    const filename = fileURLToPath(import.meta.url)
    const currentFolder = path.dirname(filename)
    const dirname = path.resolve(currentFolder, '../../')
    
    const { beforeAll, describe } = test
    
    let page: Page
    let serverURL: string
    let url: AdminUrlUtil
    
    describe('ComponentName Field Variants', () => {
      beforeAll(async ({ browser }, testInfo) => {
        testInfo.setTimeout(TEST_TIMEOUT_LONG)
        ;({ serverURL } = await initPayloadE2ENoConfig({ dirname }))
        url = new AdminUrlUtil(serverURL, componentFieldsSlug)
        const context = await browser.newContext()
        page = await context.newPage()
        initPageConsoleErrorCatch(page)
        await ensureCompilationIsDone({ page, serverURL })
      })
    
      // Test each variant from the Figma design
    })
  3. 为每个 Figma 变体编写测试:
    将步骤 2 中的变体矩阵映射到测试用例:
    typescript
    test('default state renders correctly', async () => {
      await page.goto(url.create)
      const field = page.locator('#field-componentName')
      await expect(field).toBeVisible()
      // Verify visual properties match Figma default variant
    })
    
    test('hover state shows correct styling', async () => {
      await page.goto(url.create)
      const field = page.locator('#field-componentName')
      await field.hover()
      // Verify hover styles match Figma hover variant
    })
    
    test('focus state shows correct styling', async () => {
      await page.goto(url.create)
      const field = page.locator('#field-componentName')
      await field.focus()
      // Verify focus ring/outline matches Figma focus variant
    })
    
    test('error state renders correctly', async () => {
      await page.goto(url.create)
      // Trigger validation by submitting without required field
      await page.locator('button#action-save').click()
      const field = page.locator('#field-requiredComponent')
      // Verify error styling matches Figma invalid variant
    })
    
    test('disabled state renders correctly', async () => {
      await page.goto(url.create)
      const field = page.locator('#field-disabledComponent')
      await expect(field).toBeDisabled()
      // Verify disabled styling matches Figma disabled variant
    })
    
    test('read-only state renders correctly', async () => {
      await page.goto(url.create)
      const field = page.locator('#field-readOnlyComponent')
      await expect(field).toHaveAttribute('readonly')
      // Verify read-only styling matches Figma read-only variant
    })
  4. Collection 变体已配置:(见步骤 4)
    测试 collection 应已包含步骤 4 中的所有必需变体。
  5. 运行测试进行验证:
    bash
    pnpm run test:e2e --grep "ComponentName Field Variants"

Step 8: Run ui4-review

步骤 8:运行 ui4-review

After user confirms the component looks correct, invoke the
ui4-review
skill.
This will:
  1. Scan all changed CSS files
  2. Auto-fix any remaining hardcoded values
  3. Report what was fixed/flagged

用户确认组件外观正确后,调用
ui4-review
skill。
它将:
  1. 扫描所有已更改的 CSS 文件
  2. 自动修复任何剩余的硬编码值
  3. 报告已修复或标记的问题

Reference

参考

  • Example migrated component:
    packages/ui/src/elements/Button/index.css
  • Token files:
    packages/ui/src/css/*.css
  • Legacy token migration: See Step 1 for
    var(--base)
    →
    --spacer
    conversion table
  • v4 test suite:
    test/v4/
    — dedicated collections per field type
    • Each collection should have: default, required, disabled, readOnly field variants
    • Disabled/readOnly fields need
      defaultValue
      for visible content
    • Run with:
      pnpm run dev v4
    • URL:
      http://localhost:3000/admin/collections/{slug}/create
    • Available:
      text-fields
      ,
      textarea-fields
      ,
      email-fields
      ,
      number-fields
      ,
      password-fields
      ,
      checkbox-fields
      ,
      select-fields
      ,
      relationship-fields
      ,
      upload-fields
      ,
      slug-fields
      ,
      code-fields
      ,
      json-fields
      ,
      collapsible-fields
      ,
      group-fields
      ,
      tabs-fields
      ,
      point-fields
      ,
      radio-fields
      ,
      row-fields
      ,
      array-fields
      ,
      blocks-fields
      ,
      date-fields
  • E2E test examples: See
    test/fields/collections/*/e2e.spec.ts
    for patterns
    • Test helper imports from
      test/__helpers/e2e/helpers.js
    • Use
      AdminUrlUtil
      for URL construction
    • Use
      initPayloadE2ENoConfig
      for test setup
  • 迁移后的示例组件:
    packages/ui/src/elements/Button/index.css
  • 令牌文件:
    packages/ui/src/css/*.css
  • 旧版令牌迁移: 参阅步骤 1 中的
    var(--base)
    →
    --spacer
    转换表
  • v4 测试套件:
    test/v4/
    — 每种字段类型都有专用 collection
    • 每个 collection 应包含:default、required、disabled、readOnly 字段变体
    • Disabled/readOnly 字段需要
      defaultValue
      才能显示内容
    • 运行方式:
      pnpm run dev v4
    • URL:
      http://localhost:3000/admin/collections/{slug}/create
    • 可用项:
      text-fields
      、
      textarea-fields
      、
      email-fields
      、
      number-fields
      、
      password-fields
      、
      checkbox-fields
      、
      select-fields
      、
      relationship-fields
      、
      upload-fields
      、
      slug-fields
      、
      code-fields
      、
      json-fields
      、
      collapsible-fields
      、
      group-fields
      、
      tabs-fields
      、
      point-fields
      、
      radio-fields
      、
      row-fields
      、
      array-fields
      、
      blocks-fields
      、
      date-fields
  • E2E 测试示例: 参阅
    test/fields/collections/*/e2e.spec.ts
    中的模式
    • 从
      test/__helpers/e2e/helpers.js
      导入测试辅助函数
    • 使用
      AdminUrlUtil
      构建 URL
    • 使用
      initPayloadE2ENoConfig
      设置测试