SuperAPI AI Directory

animal-island-ui-style

Frontend & websites

View repository

Updated 10-15 22:11

About this skill

Animal Island UI Design Style Guide

Division of Labor Across Three Documents(Consult on demand when generating code / adjusting styles, to avoid cross-referencing):

  • AI_USAGE.md — API manual: each component's props, types, default values, valid values, and prohibited usages.Check here first when writing code。
  • skill/SKILL.md(this document) — pixel-level styles: design tokens, precise CSS for each component (hex/px/keyframe), Demo layouts, and new component development templates.Check here when implementing/extending styles yourself。
  • DESIGN_PROMPT.md — prompt package for external tools (v0 / Figma AI / Midjourney / DALL-E), including clip-path, color palette quick reference, and prohibited list.Only use when feeding other AIs。

Overview

animal-island-ui is a React + TypeScript UI component library inspired by Animal Crossing: New Horizons. Core design language:Warm earth-tone palette + large rounded pill shapes + game-button dimensionality + soft motion + coexistence of geometric / organic shapes(Geometric examples: the swallowtail clip-path of the Title ribbon, the olive-yellow capsule of the Wallet; organic example: the SVG blob of the Modal).

  • Source code:src/components/<ComponentName>/
  • Demo site:demo/
  • Build: Vite (library mode) + vite.config.ts(library) / vite.config.demo.ts(Demo)
  • Style system: Less Modules + src/styles/variables.less design tokens

Full export list (28 components + 3 companion exports: FormItem / useForm / ICON_LIST)

From src/index.ts export:

Component Responsibility Interactive Decorative / display-only
Button Button, 5 types × 3 sizes ✓
Input Input, 3 sizes + clear/prefix/suffix ✓
Switch Switch, default/small ✓
Modal SVG blob clipped modal ✓
Drawer Sunken depth drawer (background sinks + scales + dims, four directions: left/right/top/bottom) ✓
Card Container,default/dashed, 13 solid NookPhone colors + 13 pattern polka-dot wallpapers (CSS radial-gradient, not images) ✓
Title Section title, ribbon banner (swallowtail clip-path + folded-corner shadow + slightly perspective front), 13 color schemes (replaces the old Card type="title") ✓
Collapse Accordion (animation implemented with CSS Grid 0fr↔1fr, no JS animation) ✓
Select Dropdown selector (controlled) ✓
Checkbox Checkbox group, horizontal/vertical, 3 sizes ✓
Radio Radio group, 3 sizes, keyboard roving tabindex ✓
Tooltip 12 placements,hover/focus/click trigger,default/island form ✓
Icon SVG icon library (10) ✓
Time HUD real-time clock ✓
Phone NookPhone 3×3 app grid ✓
Footer Bottom decorative image (sea/tree) ✓
Divider Decorative divider, 5 styles ✓
Cursor Game finger cursor wrapper ✓
Typewriter Typewriter effect, preserves ReactNode structure ✓
Tabs Tab switching, optional leaf-sway animation ✓
CodeBlock JSX/TS syntax-highlighted code block ✓
Loading Full-screen overlay + SVG spinner (mint #19c8b9,stroke-dasharray animation) ✓
Table Data table with fixed columns, empty state, loading ✓
Form Form container + validation (including FormItem / useForm companion exports, API like mainstream form libraries) ✓
Wallet Animal Crossing money bag style amount pill (olive yellow pill + Nook bag icon, 3 sizes, automatic thousands separator formatting) ✓
Tag Pill tag, 3 sizes × 3 variants (solid/outlined/dashed) × 12 color schemes (fully aligned with the Card palette), supports closable / onClick / disabled ✓
Notification Imperative global notification (antd style): 4 types × 6 positions, supports description / btn / onClick / key reuse update / destroy all ✓
Progress Striped scrolling progress bar: fill reuses Button loading's -45° stripes (#0ec4b6/#01b0a7) + 1s infinite scroll, 3 size levels, supports inside/right/top three text positions, infoFormat customization, duration controls fill width animation ✓

Type exports:ButtonProps/ButtonType/ButtonSize、InputProps/InputSize、SwitchProps/SwitchSize、ModalProps、DrawerProps/DrawerPlacement、CardProps/CardType/CardColor、TitleProps/TitleSize/TitleColor、FooterProps/FooterType、CollapseProps、CursorProps、TimeProps、PhoneProps、DividerProps、TypewriterProps、SelectProps/SelectOption、IconProps/IconName、TabsProps/TabItem、CheckboxProps/CheckboxOption/CheckboxSize、RadioProps/RadioOption/RadioSize、TooltipProps/TooltipPlacement/TooltipTrigger/TooltipVariant、CodeBlockProps、LoadingProps、TableProps/TableColumn、FormProps/FormItemProps/FormInstance/FormLayout/FormItemLayout/FormSize/FormLabelAlign/ColProps/NamePath/RequiredMark/RuleObject/RuleRender/RuleType/Rules/FieldData/ValidateStatus/ValidateError/ValidateInfo/ScrollOptions、WalletProps/WalletSize、TagProps/TagSize/TagVariant/TagColor、NotificationConfig/NotificationType/NotificationPosition/NotificationPlacement/NotificationItem/NotificationStatic、ProgressProps/ProgressSize/ProgressInfoPosition. Runtime values:Notification、notificationOpen、notificationDestroy、NOTIFICATION_DEFAULT_DURATION、ICON_LIST. Companion exports:FormItem、useForm(default export Form also supports Form.Item / Form.useForm syntax).


1. Design Tokens

Color System

// 主色(薄荷青绿)
@primary-color: #19c8b9;
@primary-color-hover: #3dd4c6;
@primary-color-active: #11a89b;
@primary-color-bg: #e6f9f6;

// 文字(温暖棕色系)
@text-color: #794f27; // 主文字(header/sidebar)
@text-color-body: #725d42; // 正文(组件内文字)
@text-color-secondary: #9f927d; // 次级文字
@text-color-muted: #8a7b66; // 浅棕(modal body)
@text-color-disabled: #c4b89e; // 禁用

// 边框
@border-color: #9f927d;
@border-color-light: #c4b89e; // 输入框边框
@border-color-hover: #a89878; // 输入框 hover

// 背景(奶油米白)
@bg-color: #f8f8f0; // 主背景
@bg-color-content: rgb(247, 243, 223); // 内容区(Modal、Card)
@bg-color-secondary: #f0e8d8;
@bg-color-disabled: #f0ece2;
@bg-color-input: rgb(247, 243, 223); // 输入框背景
@bg-color-input-dis: #ece8dc; // 输入框禁用

// 状态色
@success-color: #6fba2c;
@success-color-active: #5a9e1e;
@warning-color: #f5c31c;
@warning-color-active: #dba90e;
@error-color: #e05a5a;
@error-color-active: #c94444;

// 游戏特殊色
@focus-yellow: #ffcc00; // 焦点高亮(非蓝色)
@focus-yellow-dark: #e0b800; // 焦点阴影
@sidebar-active-bg: #b7c6e5; // 侧边栏选中背景
@sidebar-hover-bg: #d6dff0; // 侧边栏 hover 背景

// 3D 阴影色
@shadow-btn: #bdaea0; // 按钮 3D 阴影
@shadow-input: #d4c9b4; // 输入框 3D 阴影
@shadow-switch-on: #5a9e1e; // Switch 开启 3D 阴影

NookPhone app palette(Card color prop optional values):

color value Background color Text color
default rgb(247, 243, 223) #725d42
app-pink #f8a6b2 #fff
purple #b77dee #fff
app-blue #889df0 #fff
app-yellow #f7cd67 #725d42
app-orange #e59266 #fff
app-teal #82d5bb #fff
app-green #8ac68a #fff
app-red #fc736d #fff
lime-green #d1da49 #3d5a1a
yellow-green #ecdf52 #725d42
brown #9a835a #fff
warm-peach-pink #e18c6f #fff

Fonts

The project uses two Google Fonts rounded typefaces,mustbe imported as follows, loaded via online addresses when not installed locally:

<!-- 在 index.html <head> 中引入 -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
    href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;500;600;700;800;900&family=Noto+Sans+SC:wght@400;500;700&display=swap"
    rel="stylesheet"
/>

Or at the top of the CSS / Less entry file:

@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;500;600;700;800;900&family=Noto+Sans+SC:wght@400;500;700&display=swap');
font-family:
    Nunito,
    'Noto Sans SC',
    -apple-system,
    'PingFang SC',
    'Hiragino Sans GB',
    'Microsoft YaHei',
    sans-serif;
Font Usage Google Fonts key
Nunito Primary font, Latin characters family=Nunito
Noto Sans SC Chinese font, Simplified Chinese coverage family=Noto+Sans+SC

Historical versions once bundled the Japanese Zen Maru Gothic, which was removed in v0.9.x (the project has no Japanese UI requirements). If you need to extend Japanese characters, @import that font yourself and append it to the end of font-family .

Font weight hierarchy:

  • Body content:500
  • Button text, headings, menu items:600–700
  • Numeric emphasis (time numbers, clock):900
  • placeholder / descriptive text:400

Letter spacing:letter-spacing: 0.01em(body) / 0.02em(buttons/headings) / 1.5px(weekday uppercase)

Thin weights (weight < 400) or monospace fonts are prohibited.


Spacing / Radius / Border

间距:xs=4px  sm=8px  md=12px  lg=16px  xl=24px
圆角:sm=12px  base=18px  lg=24px  pill=50px(按钮/输入框)
边框:默认 2px solid,输入框 2.5px,大尺寸输入框 3px

Shadow

/* 卡片/容器阴影(暖色调,非冷黑)*/
box-shadow: 0 3px 10px 0 rgba(61, 52, 40, 0.1); /* 基础 */
box-shadow: 0 8px 24px 0 rgba(61, 52, 40, 0.14); /* 较大 */
/* Card 默认无 box-shadow(依赖 border / pattern 营造层次,不靠悬浮阴影)*/

/* 默认/虚线/文字/链接按钮阴影(柔和 elevation —— 非 3D 厚阴影)*/
box-shadow: 0 2px 4px 0 rgba(61, 52, 40, 0.06); /* btn-default 静止:--animal-shadow-sm */
box-shadow: 0 3px 10px 0 rgba(61, 52, 40, 0.1); /* btn-default hover:--animal-shadow-base */
/* active 回落到 --animal-shadow-sm,translateY(0) */

/* 游戏按键 3D 立体阴影(仅 primary / danger-primary 按钮;Input 仅 shadow={true} 时启用;Switch 仅 track inset 阴影,handle 无 box-shadow)*/
box-shadow: 0 5px 0 0 #bdaea0; /* primary 按钮默认 */
box-shadow: 0 6px 0 0 #bdaea0; /* primary 按钮 hover */
box-shadow: 0 1px 0 0 #bdaea0; /* primary 按钮 active */
box-shadow: 0 5px 0 0 #c94444; /* danger-primary 按钮默认(hover 6 / active 1) */
box-shadow: 0 3px 0 0 #d4c9b4; /* 输入框 shadow={true} 中号 */
box-shadow: 0 2px 0 0 #d4c9b4; /* 输入框 shadow={true} 小号 */
box-shadow: 0 4px 0 0 #d4c9b4; /* 输入框 shadow={true} 大号 */
/* Switch 仅 track 有 inset 阴影:inset 0 2px 4px rgba(114,93,66,0.15) (OFF) / inset 0 2px 4px rgba(90,158,30,0.20) (ON);handle 无 outer box-shadow */

Important: only primary style buttons (including danger primary) use 0 5px 0 0 this pixel-level 3D thick shadow;default / dashed / text / link uses the soft elevation shadow above. Applying the 3D shadow to all buttons will make the interface overly heavy and game-like.


Motion

transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); /* 通用 */
transition: all 0.15s; /* 快速(clear 按钮等)*/
transition: all 0.3s ease; /* 卡片 */
transition: grid-template-rows 0.3s cubic-bezier(0.4, 0, 0.2, 1); /* 手风琴 */

/* Hover:上浮 */
transform: translateY(-1px); /* 按钮 / 输入框 */
transform: translateY(-2px); /* 卡片 */
/* Switch handle: 始终 translateY(-50%) 垂直居中,无 hover 上浮 */

/* Active:下压(游戏按键反馈)*/
transform: translateY(2px); /* 按钮 active */

/* 出现动画 */
@keyframes animal-zoom-in {
    from {
        opacity: 0;
        transform: scale(0.92);
    }
    to {
        opacity: 1;
        transform: scale(1);
    }
}
@keyframes animal-fade-in {
    from {
        opacity: 0;
    }
    to {
        opacity: 1;
    }
}
@keyframes ac-fade-up {
    from {
        opacity: 0;
        transform: translateY(8px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

1.5 Counter-example quick reference (visual hard rules ❌/✅ pairs)

Corresponds to .cursorrules §7.3 14 contracts. Before writing code, check against this section,any occurrence of ❌ means unqualified。

1. Pure black text prohibited

  • ❌ color: #000; / color: #111;
  • ✅ color: #794f27;(primary) / #725d42(secondary) / #8a7b66(auxiliary) / #9f927d(weakened)

2. Cold blue focus ring prohibited

  • ❌ outline: 2px solid #0066ff;
  • ✅ outline: 2px solid #ffcc00;(Input/Switch/Checkbox)/ #f5c31c(Radio)/ #19c8b9(Button)

3. 0px radius prohibited / interactive elements minimum 12px

  • ❌ border-radius: 0; / border-radius: 4px;(buttons, input fields, cards, Tooltip)
  • ✅ border-radius: 50px;(buttons/input fields pill) / 20px(card) / 16px(Tooltip)

4. No cold gray backgrounds

  • ❌ background: #fafafa; / background: #f5f5f5;
  • ✅ background: #f8f8f0;(main background) / background: rgb(247, 243, 223);(content area)

5. 3D pixel stacked shadows are used only for primary / danger-primary buttons

  • ❌ <Button> Add by default box-shadow: 0 5px 0 0 #bdaea0;
  • ✅ <Button type="primary"> adds 3D shadow;type="default" uses box-shadow: 0 2px 4px 0 rgba(61, 52, 40, 0.06);

6. Input has no shadow by default

  • ❌ <Input> The component comes with a 3D shadow by default
  • ✅ <Input> no shadow;<Input shadow /> adds box-shadow: 0 3px 0 0 #d4c9b4;;status="error" not controlled by shadow , always rendered

7. Switch has no outer shadow / handle does not float up

  • ❌ handle uses box-shadow floats up / uses margin-top centered
  • ✅ handle position: absolute; top: 50%; transform: translateY(-50%); border: 2.5px solid <同色调>;

8. Card has no box-shadow

  • ❌ box-shadow: 0 4px 12px rgba(0,0,0,0.1); used for Card
  • ✅ &:hover { transform: translateY(-2px); };type="pattern" when adding border: 1.5px solid <同色调>;

9. Modal must use SVG blob clip-path

  • ❌ border-radius: 20px; paired with a rectangular container
  • ✅ clip-path: url(#animal-modal-clip); references SVG blob

10. Title is a swallowtail ribbon

  • ❌ using <Card type="title"> or with border-radius rectangular Title
  • ✅ using <Title> component, clip-path paired with transform: perspective(800px) rotateY(3deg);

11. Font

  • ❌ font-family: -apple-system, sans-serif;(UI text)
  • ✅ font-family: var(--animal-font-family, 'Nunito', 'Noto Sans SC');

12. Font weight must not be lower than 400

  • ❌ font-weight: 300; / font-weight: normal;(< 400)
  • ✅ body text 500; buttons/headings 600-700;Time/Title 900;placeholder 400

13. Motion easing

  • ❌ transition: all 0.3s ease;
  • ✅ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);(duration falls within the 0.15–0.35s range)

14. Radio is a highly rounded square + SVG checkmark

  • ❌ border-radius: 50%; paired with <span class="dot" />
  • ✅ border-radius: 14px; paired with embedded <svg> checkmark

Counterexample entries use .cursorrules §7.3 as the single source of truth; if this section conflicts with .cursorrules, .cursorrules prevails.


2. Precise component style specifications

Button

Property small middle large
height 32px 45px 48px
padding 0 16px 0 20px 0 32px
font-size 12px 14px 16px
border-radius 12px 50px 24px
border-width 2px 2px 2px

primary button precise values (only primary / danger-primary use 3D thick shadows):

color: #794f27;
background: #f8f8f0;
border-color: #f8f8f0;
font-weight: 600;
letter-spacing: 0.02em;
line-height: 1;
box-shadow: 0 5px 0 0 #bdaea0;

/* hover */
transform: translateY(-1px);
box-shadow: 0 6px 0 0 #bdaea0;

/* active */
transform: translateY(2px);
box-shadow: 0 1px 0 0 #bdaea0;

/* focus-visible */
outline: 2px solid #19c8b9;
outline-offset: 2px;

/* disabled */
opacity: 0.5;

default / dashed / text / link buttons (soft elevation):

/* 静止 */
box-shadow: var(--animal-shadow-sm); /* 0 2px 4px 0 rgba(61,52,40,0.06) */

/* hover */
color: #19c8b9;
border-color: #19c8b9;
box-shadow: var(--animal-shadow-base); /* 0 3px 10px 0 rgba(61,52,40,0.10) */
transform: translateY(-1px);

/* active */
color: #11a89b;
border-color: #11a89b;
transform: translateY(0);
box-shadow: var(--animal-shadow-sm); /* 回落到静止态 */

Do not apply the primary's 0 5px / 6px / 1px #bdaea0 to default / dashed —— the overall look will be too heavy and cartoonish.

loading diagonal stripe animation (precise values):

background: #0ec4b6;
border: 4px solid #4de2da;
color: #fff;
background-image: repeating-linear-gradient(-45deg, #0ec4b6, #0ec4b6 10px, #01b0a7 10px, #01b0a7 20px);
background-size: 28.28px 28.28px;
animation: animal-btn-loading 1s linear infinite;

@keyframes animal-btn-loading {
    0% {
        background-position: 0 0;
    }
    100% {
        background-position: -28.28px 0;
    }
}

danger primary button:

color: #fff;
box-shadow: 0 5px 0 0 #c94444; /* error-active */

Input

⚠️ shadow prop default false: no shadow by default; the following table's box-shadow only takes effect when <Input shadow /> is explicitly enabled. status (error/warning) shadows and focus yellow glow are not controlled by this prop.

Property small middle large
height 32px 40px 48px
padding 0 14px 0 18px 0 22px
font-size 12px 14px 16px
border-radius 40px 50px 50px
border-width 2.5px 2.5px 3px
box-shadow (only shadow={true}) 0 2px 0 0 #d4c9b4 0 3px 0 0 #d4c9b4 0 4px 0 0 #d4c9b4

precise color values:

background: rgb(247, 243, 223);
border: 2.5px solid #c4b89e;
/* 默认无 box-shadow;shadow={true} 时按上表中号取 0 3px 0 0 #d4c9b4 */

/* 文字 */
color: #725d42;
font-weight: 500;
letter-spacing: 0.01em;

/* placeholder */
color: #c4b89e;
font-weight: 400;

/* prefix/suffix */
color: #a0936e;

/* prefix margin-right */
margin-right: 6px;

/* suffix margin-left */
margin-left: 6px;

/* hover */
border-color: #a89878;
box-shadow: 0 3px 0 0 #c4b89e;

/* focus */
border-color: #ffcc00;
box-shadow:
    0 3px 0 0 #e0b800,
    0 0 0 3px rgba(255, 204, 0, 0.15);

/* disabled */
background: #ece8dc;
border-color: #d4c9b4;
box-shadow: none;
opacity: 0.6;
color: #c4b89e;

/* error */
box-shadow: 0 3px 0 0 #c94444;

/* warning */
box-shadow: 0 3px 0 0 #dba90e;

clear button:

width: 20px;
height: 20px;
margin-left: 4px;
color: #c4b89e;
font-size: 13px;
font-weight: 700;
border-radius: 50%;
transition: all 0.15s;
/* hover */
color: #725d42;
background: rgba(114, 93, 66, 0.1);

Switch

default size:

min-width: 52px;
height: 28px;
border: 2.5px solid #c4b89e;
border-radius: 50px;
background: #d4c9b4;
box-shadow: inset 0 2px 4px rgba(114, 93, 66, 0.15);

/* handle */
width: 21px;
height: 21px;
top: 50%;
left: 2px;
transform: translateY(-50%); /* 垂直居中 */
background: rgb(247, 243, 223);
border: 2.5px solid #bdaea0;
border-radius: 50%;
/* handle 无 outer box-shadow,仅靠 border 与 track inset 阴影分层 */

/* 开启态 */
background: #86d67a;
border-color: #6fba2c;
box-shadow: inset 0 2px 4px rgba(90, 158, 30, 0.2);
/* handle 开启后 left */
left: calc(100% - 24px);
border-color: #5a9e1e;

/* focus-visible */
outline: 2px solid #ffcc00;
outline-offset: 2px;

/* disabled */
opacity: 0.5;

small size:

min-width: 38px;
height: 20px;
border-width: 2px;
/* handle */
width: 14px;
height: 14px;
top: 1px;
left: 1px;
box-shadow: 0 2px 0 0 #bdaea0;
/* 开启 handle left */
left: calc(100% - 16px);
box-shadow: 0 2px 0 0 #5a9e1e;

inner text (checkedChildren/unCheckedChildren):

font-size: 11px;
font-weight: 700;
color: #fff;
line-height: 1;
letter-spacing: 0.02em;
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
padding: 0 8px 0 28px; /* 未开启 */
padding: 0 28px 0 8px; /* 开启 */
/* small 版 */
padding: 0 6px 0 20px;
font-size: 9px;

loading spinner:

width: 11px;
height: 11px;
border: 2px solid #6fba2c;
border-right-color: transparent;
border-radius: 50%;
animation: animal-spin 0.6s linear infinite;
/* 关闭态 */
border-color: #a89878;
@keyframes animal-spin {
    to {
        transform: rotate(360deg);
    }
}

Card

/* 默认 (无 hover) */
border-radius: 20px;
background: rgb(247, 243, 223);
padding: 16px 24px;
color: #725d42;
font-weight: 500;
/* 默认 NO box-shadow(依赖 border / pattern 分层,不靠悬浮阴影)*/
transition: all 0.3s ease;
/* 默认无 cursor:pointer、无 hover transform —— 只读卡片场景 */

/* hoverable=true 时才应用(光标 + 上浮) */
cursor: pointer;
&:hover { transform: translateY(-2px); }

/* dashed 类型 */
border: 2px dashed #e8dcc8;
background: rgb(250, 248, 242);
box-shadow: none;
/* dashed + hoverable:hover  → 只换边框色,不做位移 */
&.card-dashed:hover { transform: none; border-color: #d4c4a8; }

/* pattern 叠加(pattern !== 'none' 时,纯 CSS 实现,**无 png/svg**) */
/* 双层 radial-gradient 点阵 + 同色调 1.5px solid 边框 + pastel 浅底,
   13 种命名(default / app-pink / purple / app-blue / app-yellow / app-orange /
   app-teal / app-green / app-red / lime-green / yellow-green / brown / warm-peach-pink)
   与 Card.color 同名,但呈现为浅底波点"墙纸"而非实色块。 */
/* 例:pattern="app-pink" */
background:
    radial-gradient(circle, rgba(248, 166, 178, 0.18) 1.5px, transparent 1.5px) 0 0/28px 28px,
    radial-gradient(circle, rgba(255, 200, 210, 0.12) 1px, transparent 1px) 7px 7px/14px 14px,
    #fde4e8;
border: 1.5px solid #f8a6b2;
color: #a85565;
/* 当 color 与 pattern 同时设置时,pattern 视觉上覆盖 color */

Old Card type="title" removed in v0.9.x; for section titles please use the standalone <Title> component (see below).


Title (Ribbon section title)

Replaces the old Card type="title", rendering a game-style ribbon banner: swallowtail ends + folded corner shadow + slightly perspective front body. Source:src/components/Title/title.module.less。

/* 默认(绿色配色,可被 .color-* 覆盖) */
--rf: #27d039; /* front 正面 */
--rb: #20992a; /* back  燕尾 */
--rk: #115017; /* fold  折角阴影 */
--rt: #fff; /* text  文字色 */

font-family: Nunito, 'Noto Sans SC', sans-serif;
font-weight: 800; /* 外层 wrapper */
/* .ribbonText 内层文字 font-weight 900;padding-top 0.11em CJK 光学居中 */

/* 飘带主体 */
display: inline-flex;
height: 2em;
padding: 0 1.6em;
letter-spacing: 0.04em;
filter: drop-shadow(0 0.08em 0.12em rgba(0, 0, 0, 0.05));

/* 燕尾(左/右)—— clip-path 鱼尾形 */
.ribbonBackLeft {
    clip-path: polygon(100% 0%, 100% 100%, 0% 100%, 30% 50%, 0% 0%);
}
.ribbonBackRight {
    clip-path: polygon(0% 0%, 100% 0%, 70% 50%, 100% 100%, 0% 100%);
}
width: 1.7em;
height: 1.7em;
bottom: -0.4em;

/* 折角阴影 —— CSS border 三角 */
.ribbonFoldLeft {
    border-width: 0 0.95em 0.45em 0;
    border-color: transparent var(--rk) transparent transparent;
}
.ribbonFoldRight {
    border-width: 0 0 0.45em 0.95em;
    border-color: transparent transparent transparent var(--rk);
}

/* 正面主体 */
.ribbonFront {
    inset: 0 0.1em;
    border-radius: 0.2em;
    transform: perspective(11.5em) rotateX(3deg);
}

Size (SIZE_MAP injected via inline font-size ; all internal em auto-scale):

size font-size
small 14px
middle 20px
large 28px

13 color overrides: overlay one of .color-app-pink / .color-purple / .color-app-blue / .color-app-yellow / .color-app-orange / .color-app-teal / .color-app-green / .color-app-red / .color-lime-green / .color-yellow-green / .color-brown / .color-warm-peach-pink on the wrapper; each class simultaneously overrides --rf / --rb / --rk / --rt four variables. See details at the end of title.module.less in the 13 lines of .color-* definitions.

Example:

.color-app-yellow {
    --rf: #f7cd67;
    --rb: #d4a030;
    --rk: #8a6010;
    --rt: #725d42;
}
.color-purple {
    --rf: #b77dee;
    --rb: #9050d0;
    --rk: #5a1a9a;
    --rt: #fff;
}

Collapse

/* 外层卡片 */
border-radius: 18px;
border: 2px solid #9f927d;
margin-bottom: 12px;
/* disabled */ opacity: 0.6;

/* 问题栏 */
padding: 16px 24px;
gap: 12px;

/* 图标圆圈 */
width: 28px; height: 28px;
background: #19c8b9;
color: #fff;
border-radius: 50%;
font-size: 18px; font-weight: 700;
box-shadow: 0 2px 4px rgba(25, 200, 185, 0.3);
/* 展开时 */ transform: rotate(180deg);

/* 叶子装饰 */
opacity: 0.5;
/* 展开时 */ opacity: 1; transform: rotate(45deg);

/* 问题文字 */
font-size: 16px; font-weight: 600; line-height: 1.4;

/* 答案展开(CSS Grid trick,无 JS)*/
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 0.3s cubic-bezier(0.4, 0, 0.2, 1);
/* 展开 */ grid-template-rows: 1fr;
/* 内层 */ overflow: hidden;

/* 答案文字 */
padding: 0 24px;
font-size: 14px; line-height: 1.7;
/* 展开后 padding-bottom */ 24px;

Tabs

/* 外层容器 */
.tabs {
    background: rgb(247, 243, 223);
    border-radius: 20px;
    border: 2px solid #9f927d;
    overflow: hidden;
}

/* 标签列表 */
.tabList {
    display: flex;
    gap: 4px;
    padding: 12px;
    background: rgba(255, 255, 255, 0.6);
    border-bottom: 2px solid #c4b89e;
}

/* 标签项 */
.tabItem {
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 6px 14px;
    background: transparent;
    border: none;
    border-radius: 50px;
    cursor: pointer;
    font-size: 14px;
    font-weight: 500;
    color: #8a7b66;
    transition: all 0.2s ease;
}
/* hover */
.tabItem:hover {
    background: rgba(25, 200, 185, 0.1);
    color: #725d42;
}
/* 激活状态 — 实心 teal 胶囊 + 奶油色字 */
.tabItem.active {
    background: #0cc0b5;
    color: #fff9e3;
    font-weight: 600;
}
.tabItem.active-shadow {
    box-shadow: 0 3px 0 0 #d4c9b4; /* 仅 shadow opt-in 时启用 */
}

/* 标签图标 */
.tabIcon {
    font-size: 10px;
}
/* 激活时图标放大 */
.tabItem.active .tabIcon {
    transform: scale(1.2);
}

/* 叶子装饰动画 */
.tabLeaf {
    position: absolute;
    right: -6px;
    top: -3px;
    font-size: 12px;
    animation: leafWiggle 2s ease-in-out infinite;
}
/* leafAnimation={false} 时追加 tabLeafStatic 类去除 animation */

@keyframes leafWiggle {
    0%,
    100% {
        transform: rotate(0deg);
    }
    25% {
        transform: rotate(-10deg);
    }
    75% {
        transform: rotate(10deg);
    }
}

/* 内容区 */
.tabContent {
    padding: 24px;
    animation: fadeIn 0.25s ease;
}

@keyframes fadeIn {
    from {
        opacity: 0;
        transform: translateY(4px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

Modal

Full SVG clip-path path d value (accurately reproduces the blob outline):

<svg style={{ position: 'absolute', width: 0, height: 0 }} aria-hidden>
    <defs>
        <clipPath id="animal-modal-clip" clipPathUnits="objectBoundingBox">
            <path
                d="M0.501,0.005 L0.501,0.005 L0.523,0.005 L0.549,0.006
        C0.704,0.01,0.796,0.017,0.825,0.027
        L0.827,0.028
        C0.872,0.045,0.939,0.044,0.978,0.17
        C1,0.254,1,0.365,0.99,0.505
        L0.988,0.513
        C0.979,0.558,0.971,0.598,0.965,0.633
        C0.956,0.689,0.979,0.77,0.964,0.865
        C0.953,0.928,0.921,0.966,0.869,0.979
        C0.821,0.986,0.773,0.992,0.726,0.995
        L0.712,0.996 L0.694,0.997
        C0.648,1,0.586,1,0.507,1
        L0.501,1 L0.464,1
        C0.385,1,0.325,0.998,0.283,0.995
        C0.234,0.992,0.184,0.987,0.133,0.979
        C0.081,0.966,0.05,0.928,0.039,0.865
        C0.023,0.77,0.047,0.689,0.037,0.633
        C0.031,0.595,0.023,0.552,0.013,0.505
        C-0.006,0.365,-0.002,0.254,0.024,0.17
        C0.064,0.045,0.13,0.045,0.174,0.028
        L0.175,0.028
        C0.204,0.017,0.303,0.009,0.474,0.005
        L0.501,0.005"
            />
        </clipPath>
    </defs>
</svg>

Modal exact styles:

/* 遮罩 */
background: rgba(0, 0, 0, 0.35);
animation: animal-fade-in 0.25s ease;
z-index: 1000;

/* 弹窗容器 */
max-width: calc(100vw - 32px);
max-height: calc(100vh - 64px);
animation: animal-zoom-in 0.3s ease;

/* 裁切内容区 */
clip-path: url(#animal-modal-clip);
background: rgb(247, 243, 223);
color: rgb(128, 115, 89);
padding: 48px 48px 32px 48px;

/* 标题 */
font-size: 28px;
font-weight: 700;
color: rgba(114, 93, 66, 1);
padding-bottom: 15px;

/* 关闭按钮 */
width: 32px;
height: 32px;
font-size: 22px;
color: rgba(114, 93, 66, 0.6);
border-radius: 50%;
transition: all 0.2s;
/* hover */
background: rgba(114, 93, 66, 0.1);
color: rgba(114, 93, 66, 1);

/* body */
font-size: 20px;
font-weight: 600;
line-height: 1.6;
color: #8a7b66;
padding-bottom: 20px;

/* footer */
gap: 12px;

/* 普通按钮 */
height: 40px;
padding: 0 24px;
font-size: 18px;
border: 2px solid rgba(114, 93, 66, 0.3);
border-radius: 39.81px;
transition: all 0.2s;
line-height: 1;
/* hover */
border-color: rgba(114, 93, 66, 0.6);
background: rgba(114, 93, 66, 0.08);

/* 主按钮(确认)*/
color: rgba(114, 93, 66, 1);
background: rgba(255, 204, 0, 1); /* 游戏黄色!*/
border-color: rgba(255, 204, 0, 1);
/* hover */
background: rgba(255, 204, 0, 0.85);
border-color: rgba(255, 204, 0, 0.85);

Drawer

Sinking depth-of-field drawer — background sinks + scales + dims to highlight the subject.

/* 遮罩 — 浅黑(0.18),比 Modal 浅,让下沉的背景仍可见(景深关键)*/
position: fixed;
inset: 0;
z-index: 1000;
background: rgba(0, 0, 0, 0.18);
animation: animal-drawer-fade-in 0.25s ease;

/* 面板(通用) */
position: fixed;
z-index: 1001;
display: flex;
flex-direction: column;
background: rgb(247, 243, 223);
color: rgb(128, 115, 89);
font-family: Nunito, 'Noto Sans SC', sans-serif;
overflow: hidden;
animation-duration: 0.3s;
animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
animation-fill-mode: both;

/* 面板方向(贴视口边 0 圆角,内容边 20px)*/
/* right */
top: 0; right: 0; height: 100vh;
max-width: calc(100vw - 32px);
border-radius: 20px 0 0 20px;
box-shadow: -12px 0 32px rgba(61, 52, 40, 0.18);
animation-name: animal-drawer-slide-right; /* translateX(100%) → 0 */
/* left */
border-radius: 0 20px 20px 0;
box-shadow: 12px 0 32px rgba(61, 52, 40, 0.18);
animation-name: animal-drawer-slide-left; /* translateX(-100%) → 0 */
/* top */
border-radius: 0 0 20px 20px;
box-shadow: 0 12px 32px rgba(61, 52, 40, 0.18);
animation-name: animal-drawer-slide-top; /* translateY(-100%) → 0 */
/* bottom */
border-radius: 20px 20px 0 0;
box-shadow: 0 -12px 32px rgba(61, 52, 40, 0.18);
animation-name: animal-drawer-slide-bottom; /* translateY(100%) → 0 */

/* 标题 */
font-size: 28px;
font-weight: 700;
color: rgba(114, 93, 66, 1);

/* 关闭按钮 × */
width: 32px; height: 32px;
font-size: 22px;
color: rgba(114, 93, 66, 0.6);
border-radius: 50%;
/* hover */
background: rgba(114, 93, 66, 0.1);
color: rgba(114, 93, 66, 1);

/* body */
font-size: 20px;
font-weight: 600;
line-height: 1.6;
color: #8a7b66;
padding: 0 24px 24px 24px;

/* footer */
gap: 12px;
padding: 0 24px 24px 24px;

Background sink (JS injects inline style into non-fixed child elements of body):

transform: translateY(24px) scale(0.96);
filter: brightness(0.85) saturate(0.9);
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),
            filter 0.3s cubic-bezier(0.4, 0, 0.2, 1);

pushBackground={false} When this is the case, skip this effect and fall back to a normal overlay drawer. On close, restore the element's original inline style.


Time

/* 容器 */
display: flex; align-items: center;
gap: 20px;
padding: 16px 36px;
background: linear-gradient(180deg, #fff 0%, #f8f8f0 100%);
border: 3px solid #d4cfc3;
border-radius: 18px;
animation: ac-fade-up 0.5s ease-out;

/* 日期块(右侧分隔线)*/
padding-right: 24px;
border-right: 3px solid rgba(159, 146, 125, 0.35);

/* 星期 */
color: #6fba2c;
font-weight: 900; font-size: 14px;
letter-spacing: 1.5px;

/* 月日 */
color: #8b7355;
font-weight: 800; font-size: 22px;

/* 时间数字 */
color: #8b7355;
font-weight: 900; font-size: 48px;
letter-spacing: 2px;

/* 冒号(闪烁)*/
font-size: 48px; color: #8b7355;
position: relative; top: -0.08em;
margin: 0 1px;
animation: blink 1s step-end infinite;

@keyframes blink { 50% { opacity: 0; } }

/* 响应式 768px */
padding: 12px 20px; gap: 12px;
.acWeekday → font-size: 11px;
.acMonthday → font-size: 16px;
.acTime / .acColon → font-size: 32px;

Phone(NookPhone)

Shell (fixed size, not responsive):

.phone {
    width: 527px;
    height: 788px;
    background: #f8f4e8; /* 奶油米 */
    border-radius: 136px; /* 超大圆角,近似胶囊 */
    overflow: hidden;
}
.homeScreen {
    height: 100%;
    padding-top: 40px;
    background: #f8f4e8;
    background-size: 100% 200%;
    animation: grasswave 8s ease-in-out infinite;
    display: flex;
    flex-direction: column;
    align-items: center;
}
@keyframes grasswave {
    0%,
    100% {
        background-position: 0% 0%;
    }
    50% {
        background-position: 0% 100%;
    }
}

Top time bar:

.dateDisplay {
    padding: 0 70px 31px 70px;
    text-align: center;
}
.dateDisplayHeader {
    display: flex;
    justify-content: space-between;
    align-items: center;
    font-size: 32px;
    font-weight: 800;
    letter-spacing: 2px;
    color: #dddbcc;
}
.blink {
    font-size: 32px;
    font-weight: 800;
    color: #dddbcc;
    animation: blink 1s steps(1) infinite;
    vertical-align: text-bottom;
}
@keyframes blink {
    0%,
    50% {
        opacity: 1;
    }
    51%,
    100% {
        opacity: 0;
    }
}
.dayText {
    font-size: 48px;
    font-weight: 800;
    color: #725c4e;
    letter-spacing: 2px;
    height: 56px;
    margin-top: 20px;
}

3×3 app grid:

.appsGrid {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 32px;
    padding: 8px;
    flex: 1;
    align-content: center;
    justify-content: center;
}
.appItem {
    width: 123px;
    height: 123px;
    border-radius: 45px; /* 圆角正方形 */
    position: relative;
    display: flex;
    justify-content: center;
    align-items: center;
}
.appItem:hover .appIcon {
    animation: iconBounce 0.3s ease-in-out forwards;
}
.appIcon {
    width: 100%;
    height: 100%;
    background-repeat: no-repeat;
    background-position: center;
    background-size: 70% auto;
}
.appItemOffset {
    overflow: hidden;
}
.appIconOffset {
    transform: translateY(10px);
}

@keyframes iconBounce {
    0% {
        transform: scale(1) rotate(0deg);
    }
    50% {
        transform: scale(1.2) rotate(-5deg);
    }
    100% {
        transform: scale(1.1) rotate(-4deg);
    }
}

App data structure (src/components/Phone/Phone.tsx):

id iconClass background color offset hasNewMessage
camera iconCamera #B77DEE ✓
app iconApp #889DF0 ✓
critterpedia iconCritterpedia #F7CD67
diy iconDiy #E59266
shopping iconDesign #F8A6B2
variant iconMap #82D5BB ✓
design iconVariant #8AC68A
map iconHelicopter #FC736D
chat iconChat #D1DA49

Each iconClass is bound to a background-image: url('./img/icon-*.svg'),iconApp special uses background-size: 100% auto(the others are 70% auto). Available icon resources:icon-miles/camera/chat/critterpedia/design/diy/helicopter/map/shopping/variant.svg, as well as status icons wifi.svg / location.svg / page.svg。

Red dot (new message):

.badge {
    position: absolute;
    top: 0;
    left: 0;
    width: 28px;
    height: 28px;
    border-radius: 50%;
    background: #ff544a;
    border: 5px solid #f8f4e8; /* 奶油米描边,形成游戏风徽章 */
}

Bottom status icons:

.iconWifi {
    width: 79px;
    height: 29px;
    background: url('./img/wifi.svg') center/contain no-repeat;
}
.iconLocation {
    width: 36px;
    height: 36px;
    background: url('./img/location.svg') center/contain no-repeat;
}
.iconPage {
    width: 65px;
    height: 32px;
    background: url('./img/page.svg') center/contain no-repeat;
}
.pageIndicator {
    display: flex;
    justify-content: center;
    align-items: center;
    margin-top: 74px;
}

Behavior: inside useEffect + setInterval(1000) updates the time,12 小时制 + AM/PM + 零填充分钟, the colon blinks with a 1s period. The component has no business callbacks, it is purely presentational.


Footer

<Footer />              // 默认:森林(tree,高 60px)
<Footer type="sea" />   // 海浪(高 80px)
.footer {
    width: 100%;
    height: 80px;
    background: url('./img/footer-sea.svg') center/contain no-repeat;
}
.tree {
    background-image: url('./img/footer-tree.webp');
    height: 60px;
    background-size: cover;
    background-position: bottom center;
}
  • sea: SVG wave illustration,viewBox="0 0 1440 186", multi-color (coral #EC7175, sea blue #327A93, light blue #98D2E3, deep cyan #008077 , etc.).
  • tree: webp forest silhouette, placed at the very bottom of the page.

Divider

<Divider type="line-brown" />  // 默认
<Divider type="line-teal" />
<Divider type="line-white" />
<Divider type="line-yellow" />
<Divider type="wave-yellow" />
.divider {
    width: 100%;
    height: 12px;
    background: url('./img/divider-line-brown.svg') center/contain no-repeat;
}
.line-teal {
    background-image: url('./img/divider-line-teal.svg');
}
.line-white {
    background-image: url('./img/divider-line-white.png');
}
.line-yellow {
    background-image: url('./img/divider-line-yellow.svg');
}
.wave-yellow {
    background-image: url('./img/wave-yellow.svg');
}

Default SVG color reference:#D8D0C3(beige-brown),viewBox="0 0 297 14"。


Cursor

<Cursor>
    <App /> {/* 此范围内所有元素变为游戏手指光标 */}
</Cursor>

The style file is plain CSS(not a module):

.animal-cursor,
.animal-cursor * {
    cursor:
        url('./cursor-icon.png') 4 0,
        auto !important;
}
  • cursor-icon.png hotspot coordinates (4, 0)
  • uses !important to override the default cursor;className is attached directly to the root <div> , with a fixed class name of animal-cursor

Typewriter

<Typewriter speed={90} trigger={openCount} autoPlay onDone={() => ...}>
  <p>第一行 <strong>加粗</strong></p>
  <p>第二行</p>
</Typewriter>

Props:

name type default Description
children ReactNode — The content to be typed out character by character,preserving the original element structure / line breaks / styles
speed number (ms) 90 Interval per character
trigger unknown — A change in value triggers replay (usually pass the popup open count or an incrementing key)
autoPlay boolean true false Display the full content directly
onDone () => void — Playback complete callback

Implementation points:

  • countText(node): recursively count the plain text length of a ReactNode
  • renderTruncated(node, state): recursively trim by remaining character count,React.cloneElement preserves the original nodes and styles
  • useEffect depends on [total, speed, trigger, autoPlay], internally setInterval increments count
  • No style file, does not wrap any extra DOM (returns <>...</>), zero impact on layout

Checkbox

Props:

name type default Description
options CheckboxOption[] — Required; each item's { label, value, disabled? }
value Array<string | number> — Controlled selected value
defaultValue Array<string | number> [] Uncontrolled default value
size 'small' | 'middle' | 'large' 'middle' Size
disabled boolean false Disable all items
direction 'horizontal' | 'vertical' 'horizontal' Arrangement direction
onChange (values) => void — Selected value change

Size table (box square):

Property small middle large
Width/Height 18×18px 22×22px 28×28px
border-width 2px 2.5px 3px
Label font-size 12px 14px 16px
Checkmark font-size 11px 13px 16px

Exact styles:

/* group */
display: flex; flex-wrap: wrap;
gap: 12px;                                 /* horizontal */
/* vertical */ flex-direction: column; gap: 8px;

/* item */
display: inline-flex; align-items: center;
gap: 8px;
cursor: pointer;
transition: all 0.25s cubic-bezier(0.4,0,0.2,1);

/* box(未选)*/
background: rgb(247, 243, 223);
border: 2.5px solid #c4b89e;
border-radius: 8px;
display: inline-flex; align-items: center; justify-content: center;

/* box hover */
border-color: #19c8b9;
transform: translateY(-1px);

/* box focus-visible */
outline: 2px solid #ffcc00; outline-offset: 2px;

/* 选中 */
background: #19c8b9;
border-color: #11a89b;
/* 选中 hover */ background: #3dd4c6; border-color: #19c8b9;

/* 对勾 ✓ */
color: #fff; font-weight: 700; line-height: 1;
animation: animal-checkbox-pop 0.15s cubic-bezier(0.4,0,0.2,1);

@keyframes animal-checkbox-pop {
  0%   { transform: scale(0.4); opacity: 0; }
  60%  { transform: scale(1.2); }
  100% { transform: scale(1);   opacity: 1; }
}

/* label */
color: #725d42; font-weight: 500;
letter-spacing: 0.01em;
/* item hover */ label color: #794f27;

/* 禁用(单项或整组)*/
cursor: not-allowed;
opacity: 0.55;
/* box */ background: #f0ece2; border-color: #d4c9b4; transform: none !important;
/* label */ color: #c4b89e;

CodeBlock

Props:

name type default Description
code string — Required; the original source code string, internally tokenized and highlighted as JSX/TS
style CSSProperties — Merges with and overrides the default dark theme
className string — Custom class name

Default theme (hardcoded in the component, does not use Less):

padding: 20px 24px;
background: #2b2118;
border: 1px solid #3d3028;
border-radius: 20px;
font-size: 14px;
line-height: 1.7;
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', Consolas, monospace;
font-weight: 600;
color: #e8d5bc;
white-space: pre;
overflow: auto;
tab-size: 4;

Token palette (COLORS constants):

| token | color | override | | --------- | --------- | ------------------------------------------------------------------- | -------- | | comment | #6b5e50 | /* */、// | | string | #a8d4a0 | backticks / single and double quotes, numbers | | keyword | #d4a0e0 | import/export/const/return/async/...、true/false/null/undefined | | react | #e06c75 | React/useState/useEffect/FC/ReactNode/CSSProperties/... | | component | #80c0e0 | UpperCamelCase identifiers (JSX component names, type names) | | func | #61afef | lowercase identifier followed by ( | | prop | #e8c87a | identifier followed by =(JSX props / assignment) | | jsx | #f0a870 | <Tag、</Tag、/> | | operator | #d4b896 | {}[]();, and +-\*/=<>& | ^~?: , etc. | | default | #e8d5bc | all other text |

Does not support the language prop; non-JS/TS code (Python/Shell/SQL) is colored by generic rules, so the display may be inaccurate. No copy button, line numbers, or line wrapping.


Radio

Source:src/components/Radio/radio.module.less。

Property small middle large
Outer box size 18×18px 22×22px 28×28px
Corner radius 12px 14px 16px(Rounded-square, not a perfect circle)
Border width 2px 2px 2px
Inner check size 10×10px 12×12px 16px font-size
label font-size 12px 14px 16px
/* 默认(未选) */
background: rgb(247, 243, 223);
border: 2px solid #c4b89e;

/* hover */
border-color: #19c8b9;
transform: translateY(-1px);

/* checked */
background: #19c8b9; /* @primary-color */
border-color: #11a89b; /* @primary-color-active */
/* 内白色勾 pop 动画 */
@keyframes radio-pop {
    0% {
        transform: scale(0.4);
        opacity: 0;
    }
    60% {
        transform: scale(1.2);
    }
    100% {
        transform: scale(1);
        opacity: 1;
    }
}
/* 时长 0.15s ease(@motion-duration-fast) */

/* label */
color: #725d42;
font-weight: 500;
letter-spacing: 0.01em;
/* checked label */
color: #794f27;

/* focus-visible */
outline: 2px solid #f5c31c; /* 注意:Radio 用 @focus-yellow=#f5c31c,而非 Checkbox/Input 的 #ffcc00 */
outline-offset: 2px;

/* disabled */
opacity: 0.55;
cursor: not-allowed;
background: #f0ece2;
border-color: #d4c9b4;
/* label */
color: #c4b89e;

/* group 布局 */
/* horizontal */
display: flex;
gap: 12px;
/* vertical */
display: flex;
flex-direction: column;
gap: 8px;

Tooltip

Source:src/components/Tooltip/tooltip.module.less。default and island are twocompletely differentvisuals; do not confuse them.

default variant (standard warm-colored bubble):

background: rgb(247, 243, 223); /* @tooltip-bg */
border: 2px solid #c4b89e; /* @tooltip-border */
border-radius: 16px; /* @border-radius-sm */
padding: 6px 12px;
max-width: 240px;

font-size: 12px;
font-weight: 500;
line-height: 1.5;
letter-spacing: 0.01em;
color: #725d42;

box-shadow: 0 3px 10px rgba(61, 52, 40, 0.1); /* @shadow-base */
z-index: 100;

/* 距 trigger 间距 */
gap: 10px;
/* 入场动画:translateY 4px → 0,平滑显隐 */

/* 三角箭头 */
size: 8px;
border-radius: 2px; /* 8px 菱形,圆角 2px —— 不是 6px */

island variant (transparent organic bubble):

background: transparent; /* 容器透明,无 border 无 shadow */
border: none;
box-shadow: none;
/* 注意:island **不是** Modal blob clip-path —— 它是 transparent 容器 + 内部内容自带气泡 */

/* 内容区 */
padding: 12px 20px;
max-width: 280px;
font-weight: 600;
line-height: 1.55;
text-align: center;

/* 箭头:14px 圆点(borderless)或 10px 菱形(bordered) */
.islandArrow {
    width: 14px;
    height: 14px;
    border-radius: 50%;
    filter: drop-shadow(0 4px 14px rgba(121, 79, 39, 0.14));
}

12 placements:top / top_start / top_end / bottom / bottom_start / bottom_end / left / left_start / left_end / right / right_start / right_end。


Loading (full-screen overlay)

Source:src/components/Loading/loading.module.less。The project has no GSAP / MotionPath, all implemented with native CSS + SVG stroke-dasharray .

/* 容器 */
position: absolute; /* 不是 fixed —— 受最近的 positioned 父级约束 */
inset: 0;
background: black; /* 不是 #f8f8f0 */
overflow: hidden;

/* 揭示遮罩(消失时 mask 半径渐变) */
--mask-r: 0;
mask: radial-gradient(circle at center, transparent var(--mask-r), black calc(var(--mask-r) + 1px));
/* active=false 时把 --mask-r 过渡到大值,得到圆形渐隐效果 */

/* SVG spinner */
color: #19c8b9; /* @primary-color mint teal */
animation: spin 1s linear infinite;
@keyframes spin {
    to {
        transform: rotate(360deg);
    }
}

/* 内圈 circle 的 dash 动画 */
animation: dash 1.5s ease-in-out infinite;
@keyframes dash {
    0% {
        stroke-dasharray: 1, 150;
        stroke-dashoffset: 0;
    }
    50% {
        stroke-dasharray: 90, 150;
        stroke-dashoffset: -35;
    }
    100% {
        stroke-dasharray: 90, 150;
        stroke-dashoffset: -124;
    }
}

The button inline loading stripes (-45deg mint stripes 28.28px) belong to the Button component; do not confuse them with the <Loading> full-screen overlay.


Table

Source:src/components/Table/table.module.less。The shell has no solid border; row separation is achieved with the ::after dashed horizontal line; the hovered row has diagonal cyan stripes.

/* 外壳 wrapper */
background: rgb(247, 243, 223);
border-radius: 20px;
padding: 6px; /* 仅 6px 内边距,无 border */
box-sizing: border-box;

/* 表头 cell */
padding: 16px 20px;
font-size: 14px;
font-weight: 700;
color: #725d42; /* 不是 #794f27 */
letter-spacing: 0.02em;
/* 表头底部分隔(::after dashed) */
border-image: none;
&::after {
    content: '';
    border-bottom: 1px dashed rgb(240, 232, 216);
    /* dash pattern: 6px on / 6px off */
}

/* body cell */
padding: 14px 20px; /* 无固定行高 48px —— 由 padding 撑起 */
font-size: 14px;
font-weight: 500;
color: #725d42;
line-height: 1.6;
/* 行底分隔线同样是 1px dashed (6/6) rgb(240,232,216) */

/* striped 偶数行 */
background: rgba(248, 248, 240, 0.6); /* 不是 rgba(247,243,223,0.5) */

/* row hover —— 对角青色条纹 + 内圆角剪切 */
background: repeating-linear-gradient(-45deg, rgba(25, 200, 185, 0.6) 0 10px, rgba(14, 196, 182, 0.6) 10px 20px);
background-size: 28.28px 28.28px;
clip-path: inset(0 0 0 0 round 30px);
color: #3d2e1e;

/* 空状态 */
padding: 60px 20px;
text-align: center;
color: #9f927d;
/* icon */
opacity: 0.5;

/* loading 遮罩 */
background: rgba(247, 243, 223, 0.8);
backdrop-filter: blur(2px);
/* spinner */
color: #19c8b9;

Form

Source:src/components/Form/Form.tsx + FormItem.tsx + useForm.ts + Form.module.less + types.ts。Form container: an API similar to mainstream form libraries, providing Form.useForm() instances,<Form.Item> field registration, validation, submission, reset, and other imperative capabilities.FormItem via React.cloneElement hijacks the child control's value / onChange(controlled injection); child controls do not need to implement controlled logic themselves.

/* 容器 .island-form —— 沿用中性灰色系,不走 parchment 暖棕系 */
.island-form {
    color: rgba(0, 0, 0, 0.85); /* label / 正文 —— 故意不是 #725d42 */
    font-size: 14px;
    line-height: 1.6;
    box-sizing: border-box;
}

/* horizontal:垂直堆叠,每个 item 是 24 列 CSS Grid(label/wrapper 相邻无 column-gap)*/
.island-form-horizontal {
    display: flex;
    flex-direction: column;
    gap: 8px; /* @form-item-gap */
}
.island-form-horizontal .island-form-item {
    display: grid;
    grid-template-columns: repeat(24, minmax(0, 1fr));
    align-items: baseline;
    row-gap: 8px;
    /* 注意:label 与 wrapper 之间不加 column-gap,否则 23 × 16px = 368px 撑爆 form */
}

/* vertical:item 是 block,label 独占一行 */
.island-form-vertical .island-form-item {
    display: block;
}
.island-form-vertical .island-form-item-label {
    display: block;
    margin-bottom: 6px; /* @vertical-gap */
}

/* inline:item 横向排开 */
.island-form-inline {
    display: flex;
    flex-wrap: wrap;
    gap: 8px; /* @inline-gap */
}
.island-form-inline .island-form-item {
    flex: 0 0 auto;
}

/* label */
.island-form-item-label {
    color: rgba(0, 0, 0, 0.85);
    font-weight: normal;
    white-space: nowrap;
    line-height: 1.6;
}
.island-form-item-label-required::before {
    content: '*';
    color: #ff4d4f;
    margin-right: 4px;
}
.island-form-item-label-colon::after {
    content: ':';
    margin: 0 4px 0 2px;
}

/* 尺寸只缩放 label 字号 */
.island-form-small .island-form-item-label { font-size: 12px; }
.island-form-middle .island-form-item-label { font-size: 14px; } /* default */
.island-form-large .island-form-item-label { font-size: 16px; }

/* 控件容器 + 错误文案槽 */
.island-form-item-control-input {
    position: relative;
    display: flex;
    align-items: center;
    min-height: 32px;
}
.island-form-item-explain {
    min-height: 22px;
    color: rgba(0, 0, 0, 0.45);
    font-size: 12px;
    line-height: 1.5;
    margin-top: 4px;
}
/* 状态色(采用中性灰 + 主流状态色,不走 parchment token)*/
.island-form-item-has-error  .island-form-item-explain { color: #ff4d4f; }
.island-form-item-has-warning.island-form-item-explain { color: #faad14; }
.island-form-item-has-success.island-form-item-explain { color: #52c41a; }
.island-form-item-is-validating .island-form-item-explain { color: #1677ff; }

/* 全局 disabled */
.island-form-disabled {
    cursor: not-allowed;
    opacity: 0.6;
}

Form's internal neutral gray rgba(0,0,0,0.85) / #ff4d4f / #faad14 / #52c41a / #1677ff intentionally does not use src/styles/variables.less the parchment warm-brown token, in order to match the visual conventions of mainstream form libraries (users have established expectations for these state colors in form scenarios). Do not "for the sake of consistency" change them to #725d42 / #e05a5a etc.

FormItem must be used inside <Form> or <Form.Provider> , otherwise it throws Form.Item must be used inside <Form>。disabled / size / status="error" The three props are passed through to child controls via cloneElement.


Wallet

Source:src/components/Wallet/Wallet.tsx + wallet.module.less。Amount display component: olive-yellow pill + protruding Animal Crossing money bag icon, numbers with stroke. 3 size presets (CSS variable driven), numbers automatically formatted with thousands separators.

@wallet-pill-fill:   #b3a046; /* 橄榄黄主色 */
@wallet-pill-shadow: #8e7d2c;
@wallet-halo:        #fffbe7; /* 奶油外发光 */
@wallet-text:        #ffffff;
@wallet-text-shadow: rgba(91, 78, 30, 0.55);

/* 根容器 —— inline-flex,顶部留出钱袋上凸空间 */
.wallet {
    --wallet-pill-w: 132px;
    --wallet-pill-h: 42px;
    --wallet-bag: 50px;
    --wallet-text-size: 17px;
    --wallet-halo: 4px;
    position: relative;
    display: inline-flex;
    flex-direction: column;
    align-items: center;
    width: var(--wallet-pill-w);
    padding-top: calc(var(--wallet-bag) * 0.7); /* 70% 钱袋上凸 */
    user-select: none;
    line-height: 1;
}

/* 尺寸预设 */
.size-small  { --wallet-pill-w: 96px;  --wallet-pill-h: 32px; --wallet-bag: 38px; --wallet-text-size: 12px; --wallet-halo: 3px; }
.size-large  { --wallet-pill-w: 176px; --wallet-pill-h: 54px; --wallet-bag: 66px; --wallet-text-size: 22px; --wallet-halo: 6px; }
/* medium 是默认,不写修饰类 */

/* 钱袋 slot(图标):absolute 上凸于 pill */
.bagSlot {
    position: absolute;
    left: 50%;
    top: 0;
    width: var(--wallet-bag);
    height: var(--wallet-bag);
    transform: translateX(-50%);
    display: flex;
    align-items: center;
    justify-content: center;
    pointer-events: none;
    z-index: 2;
    filter: drop-shadow(0 4px 6px rgba(91, 78, 30, 0.18));
}

/* pill 本体:胶囊 + 多层 box-shadow */
.pill {
    position: relative;
    width: 100%;
    height: var(--wallet-pill-h);
    border-radius: 999px;
    background: @wallet-pill-fill;
    /* 多层阴影:内暗部 + 内描边 + 外 halo + 投影 */
    box-shadow:
        inset 0 -6px 0 rgba(91, 78, 30, 0.18),
        inset 0 0 0 2px rgba(91, 78, 30, 0.12),
        0 0 0 var(--wallet-halo) @wallet-halo,
        0 6px 14px rgba(91, 78, 30, 0.18);
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: visible;
}

/* 数字文本 */
.value {
    font-family: 'Nunito', 'Noto Sans SC', system-ui, sans-serif;
    font-weight: 800;
    font-size: var(--wallet-text-size);
    color: @wallet-text;
    letter-spacing: 0.04em;
    /* 双层 text-shadow 模拟描边 */
    text-shadow:
        0 2px 0 @wallet-text-shadow,
        0 0 1px @wallet-text-shadow;
    font-variant-numeric: tabular-nums; /* 防数字跳动 */
    padding: 0 12px;
    white-space: nowrap;
}

/* hover:钱袋 0.5s 跳动 */
.wallet:hover .bagSlot {
    animation: walletBagBounce 0.5s ease-in-out;
}
@keyframes walletBagBounce {
    0%, 100% { transform: translateX(-50%) translateY(0) rotate(0deg); }
    35%      { transform: translateX(-50%) translateY(-8px) rotate(-6deg); }
    70%      { transform: translateX(-50%) translateY(-2px) rotate(3deg); }
}

Number formatting (JS logic, not CSS):value is number , insert thousands separators thousandSeparator(default ,, pass "" close);string displayed as-is;undefined / null shows 00,000。

The default money bag icon is the built-in assets/img/icons/items/item-022.png(Animal Crossing-style money bag), which can be replaced by passing any icon via the ReactNode prop. Note: internally it uses the hidden <Icon src={bagIcon} /> parameter of src (Icon supports both name going through ICON_LIST, and src going through any image source).


Icon

Source:src/components/Icon/Icon.tsx. SVG monochrome icon library, 10 built-in icons (arrow-down, arrow-up, check, close, copy, leaf, menu, search, star, trash, etc. — subject to the runtime ICON_LIST exports). Supports name(looks up ICON_LIST) and the hidden src(any image source; Wallet internally uses this to load the money bag PNG).

.icon {
    display: inline-block;
    vertical-align: middle;
    fill: currentColor; /* 颜色继承父级 color */
    width: 1em;
    height: 1em;
}

Usage:<Icon name="check" size={20} color="#19c8b9" />。size defaults to 16,color defaults to currentColor。src mode goes through <img> rendering, and can be used for any icon source outside the built-in icon library.


Select

Source:src/components/Select/Select.tsx. Controlled dropdown selector; hover/click expands the dropdown panel; options support keyboard ↑/↓ navigation + Enter to confirm + Esc to cancel.

.select {
    position: relative;
    display: inline-block;
    min-width: 120px;
}
.selectTrigger {
    /* 与 Input 同款:border 1.5px solid @border-color-light,radius 12px */
    /* 背景 rgb(247,243,223),hover/focus 切换到 @border-color-hover */
}
.selectDropdown {
    position: absolute;
    top: calc(100% + 4px);
    left: 0;
    right: 0;
    max-height: 240px;
    overflow-y: auto;
    background: #fffcef;
    border: 1.5px solid @border-color-light;
    border-radius: 12px;
    box-shadow: 0 6px 18px rgba(61, 52, 40, 0.12);
    z-index: 1000;
}
.selectOption {
    padding: 8px 12px;
    cursor: pointer;
    color: @text-color-body;
}
.selectOption.isActive { background: @primary-color-bg; color: @primary-color; }
.selectOption.isSelected { font-weight: 600; }

For the full interaction (keyboard roving, scroll to selected item, click outside to close), see src/components/Select/Select.tsx; for the API, see AI_USAGE.md §1。


Tag

Source:src/components/Tag/Tag.tsx + tag.module.less。Pill tag: fully aligned with the Card palette (12 brand colors + 1 default), 3 sizes × 3 variants (solid / outlined / dashed), supports closable / onClick / disabled.

// 根容器 —— 全胶囊,1.5px transparent border(为变体预留空间,避免抖动)
.tag {
    box-sizing: border-box;
    display: inline-flex;
    align-items: center;
    gap: 4px;
    line-height: 1;
    font-family: inherit;
    font-weight: 600;
    border-radius: 999px;
    border: 1.5px solid transparent;
    transition: all 0.2s ease;
    user-select: none;
    white-space: nowrap;
}

// ---------- Size ----------
.size-small  { height: 24px; line-height: 21px; padding: 0 10px; font-size: 12px; }
.size-medium { height: 29px; line-height: 26px; padding: 0 12px; font-size: 13px; } /* 默认 */
.size-large  { height: 34px; line-height: 31px; padding: 0 16px; font-size: 15px; }

// ---------- Variant ----------
.variant-solid    { background: rgb(247, 243, 223); color: #8f734f; border-color: #d4c4a8; }
.variant-outlined { background: transparent;    color: #8f734f; border-color: #c4b89e; }
.variant-dashed   { background: transparent;    color: #8f734f; border-color: #c4b89e; border-style: dashed; }

// ---------- Color(与 Card 的 .pattern-{color} 边框色完全一致) ----------
// solid 变体:背景 = 饱和色,文字 #fff
.color-app-pink-solid         { background: #f8a6b2; border-color: #f8a6b2; color: #fff; }
.color-purple-solid           { background: #b77dee; border-color: #b77dee; color: #fff; }
.color-app-blue-solid         { background: #889df0; border-color: #889df0; color: #fff; }
.color-app-yellow-solid       { background: #f7cd67; border-color: #f7cd67; color: #fff; }
.color-app-orange-solid       { background: #e59266; border-color: #e59266; color: #fff; }
.color-app-teal-solid         { background: #82d5bb; border-color: #82d5bb; color: #fff; }
.color-app-green-solid        { background: #8ac68a; border-color: #8ac68a; color: #fff; }
.color-app-red-solid          { background: #fc736d; border-color: #fc736d; color: #fff; }
.color-lime-green-solid       { background: #d1da49; border-color: #d1da49; color: #fff; }
.color-yellow-green-solid     { background: #ecdf52; border-color: #ecdf52; color: #fff; }
.color-brown-solid            { background: #9a835a; border-color: #9a835a; color: #fff; }
.color-warm-peach-pink-solid  { background: #e18c6f; border-color: #e18c6f; color: #fff; }

// outlined / dashed 变体:文字 + 边框 = 饱和色,背景透明
.color-app-pink-outlined,
.color-app-pink-dashed         { color: #f8a6b2; border-color: #f8a6b2; }
.color-purple-outlined,
.color-purple-dashed           { color: #b77dee; border-color: #b77dee; }
.color-app-blue-outlined,
.color-app-blue-dashed         { color: #889df0; border-color: #889df0; }
.color-app-yellow-outlined,
.color-app-yellow-dashed       { color: #f7cd67; border-color: #f7cd67; }
.color-app-orange-outlined,
.color-app-orange-dashed       { color: #e59266; border-color: #e59266; }
.color-app-teal-outlined,
.color-app-teal-dashed         { color: #82d5bb; border-color: #82d5bb; }
.color-app-green-outlined,
.color-app-green-dashed        { color: #8ac68a; border-color: #8ac68a; }
.color-app-red-outlined,
.color-app-red-dashed          { color: #fc736d; border-color: #fc736d; }
.color-lime-green-outlined,
.color-lime-green-dashed       { color: #d1da49; border-color: #d1da49; }
.color-yellow-green-outlined,
.color-yellow-green-dashed     { color: #ecdf52; border-color: #ecdf52; }
.color-brown-outlined,
.color-brown-dashed            { color: #9a835a; border-color: #9a835a; }
.color-warm-peach-pink-outlined,
.color-warm-peach-pink-dashed  { color: #e18c6f; border-color: #e18c6f; }

// ---------- Close button ----------
.close {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    margin-left: 2px;
    margin-right: -4px;
    width: 16px;
    height: 16px;
    padding: 0;
    border: none;
    background: rgba(0, 0, 0, 0.08);
    color: inherit;
    font-size: 14px;
    line-height: 1;
    border-radius: 50%;
    cursor: pointer;
    transition: background 0.15s ease;
}
.close:hover { background: rgba(0, 0, 0, 0.18); }
.close:disabled { cursor: not-allowed; opacity: 0.5; }

// ---------- Interactive ----------
.is-clickable { cursor: pointer; }
.is-clickable:hover {
    transform: translateY(-1px);
    box-shadow: 0 2px 6px rgba(61, 52, 40, 0.12);
}
.is-clickable:active { transform: translateY(0); }
.is-clickable:focus-visible {
    outline: 2px solid var(--animal-focus-yellow, #f5c31c);
    outline-offset: 2px;
}
.is-disabled { opacity: 0.5; cursor: not-allowed; pointer-events: none; }

Key design decisions:

  • Shares the same 12-color palette with Card (directly reuses its pattern-{color} border color), ensuring visual consistency for the "card + tag" combination.
  • border: 1.5px solid transparent default placeholder, so that switching between outlined/dashed does not cause size jitter due to the border appearing/disappearing.
  • closable × button's click stopPropagation, does not bubble up to trigger onClick。
  • provides onClick , the entire tag is upgraded to role="button" + tabIndex={0}, supporting Enter / Space keyboard triggering.

Notification

Source:src/components/Notification/NotificationPortal.tsx(imperative API + global store + container) + Notification.tsx(single item view) + notification.module.less。 Imperative component(similar to antd): no <Notification> JSX element; everything is triggered via Notification.open / .success / .info / .warning / .error / .destroy ; on first call, a root container is mounted at document.body (data-animal-notification-root), and subsequent useSyncExternalStore subscribes to store changes. SSR-safe (typeof document guard).

// 6 种静态方法,config 可以是字符串(仅 message)或完整对象
Notification.open({ message: '...', description: '...', type: 'info', position: 'top', duration: 4.5 });
Notification.success('保存成功!');
Notification.error({ message: '网络错误', description: '请稍后重试' });
Notification.warning('库存不足');
Notification.info('系统通知');
Notification.destroy();        // 关闭全部
Notification.destroy('upload'); // 关闭指定 key

Root container (viewport layer):

.notificationRoot {
    position: fixed;
    inset: 0;
    pointer-events: none; /* 通知自身 pointer-events: auto,根容器透传 */
    z-index: 2000; /* 高于 Modal(1000)/Drawer(1001) */
}

/* 6 个 fixed 容器,各自按位置对齐,内层 column 堆叠 */
.position-top, .position-topLeft, .position-topRight,
.position-bottom, .position-bottomLeft, .position-bottomRight {
    position: fixed;
    display: flex;
    flex-direction: column;
    gap: 12px;
    pointer-events: none;
    max-width: calc(100vw - 32px);
}
.position-top       { top: 24px; left: 50%; transform: translateX(-50%); align-items: center; }
.position-topLeft   { top: 24px; left: 24px; align-items: flex-start; }
.position-topRight  { top: 24px; right: 24px; align-items: flex-end; }
.position-bottom       { bottom: 24px; left: 50%; transform: translateX(-50%); align-items: center; flex-direction: column-reverse; }
.position-bottomLeft   { bottom: 24px; left: 24px; align-items: flex-start; flex-direction: column-reverse; }
.position-bottomRight  { bottom: 24px; right: 24px; align-items: flex-end; flex-direction: column-reverse; }

The top group (top*) stacks from top to bottom, with new notifications appended to the end of the queue; the bottom group (bottom*) uses flex-direction: column-reverse so that new notifications appear at the very bottom.

Single card (exact values):

.notification {
    pointer-events: auto;
    box-sizing: border-box;
    display: flex; align-items: flex-start; gap: 12px;
    width: 384px;
    max-width: calc(100vw - 48px);
    padding: 14px 16px;
    background: rgb(247, 243, 223);
    border: 2px solid #c4b89e; /* 默认无 type 时 */
    border-radius: 18px;
    box-shadow: 0 6px 18px rgba(61, 52, 40, 0.14);
    color: #725d42;
    transition: box-shadow 0.2s ease, transform 0.2s ease;
    will-change: transform, opacity;
}

/* 4 种 type 覆盖 border + shadow + 图标底色 */
.type-success  { border-color: #6fba2c; box-shadow: 0 6px 18px rgba(111, 186, 44, 0.18); }
.type-success  .iconWrap { background: #e9f6d4; color: #5a9e1e; }
.type-info     { border-color: #19c8b9; box-shadow: 0 6px 18px rgba(25, 200, 185, 0.18); }
.type-info     .iconWrap { background: #e6f9f6; color: #11a89b; }
.type-warning  { border-color: #f5c31c; box-shadow: 0 6px 18px rgba(245, 195, 28, 0.20); }
.type-warning  .iconWrap { background: #fdf3c4; color: #b88a06; }
.type-error    { border-color: #e05a5a; box-shadow: 0 6px 18px rgba(224, 90, 90, 0.18); }
.type-error    .iconWrap { background: #fbe0e0; color: #c94444; }

/* 可点击时(onClick 提供) */
.clickable { cursor: pointer; }
.clickable:hover  { box-shadow: 0 10px 26px rgba(61, 52, 40, 0.18); transform: translateY(-1px); }
.clickable:focus-visible { outline: 2px solid #ffcc00; outline-offset: 2px; }

Structure (single item):

.iconWrap { /* 32×32 圆 */
    flex: 0 0 auto;
    width: 32px; height: 32px;
    border-radius: 50%;
    display: inline-flex; align-items: center; justify-content: center;
    font-size: 18px; line-height: 1;
    user-select: none;
    /* 颜色由 .type-* 注入 */
}
.body { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
.title       { font-size: 15px; font-weight: 700; line-height: 1.4; color: #794f27; letter-spacing: 0.01em; }
.description { font-size: 13px; font-weight: 500; line-height: 1.55; color: #8a7b66; letter-spacing: 0.01em; }
.btnSlot { flex: 0 0 auto; margin-left: 4px; }
.close { /* 22×22 圆 × 按钮 */
    width: 22px; height: 22px;
    color: rgba(114, 93, 66, 0.55);
    background: transparent;
    border-radius: 50%;
    font-size: 18px; line-height: 1;
    transition: all 0.15s ease;
}
.close:hover         { background: rgba(114, 93, 66, 0.12); color: rgba(114, 93, 66, 1); }
.close:focus-visible { outline: 2px solid #ffcc00; outline-offset: 2px; }

Enter / exit animations (direction based on placement):

.placement-top    { animation: animal-notification-slide-down 0.25s cubic-bezier(0.4,0,0.2,1) both; }
.placement-top.leaving    { animation: animal-notification-slide-up 0.25s cubic-bezier(0.4,0,0.2,1) both; }
.placement-bottom { animation: animal-notification-slide-up   0.25s cubic-bezier(0.4,0,0.2,1) both; }
.placement-bottom.leaving { animation: animal-notification-slide-down 0.25s cubic-bezier(0.4,0,0.2,1) both; }

@keyframes animal-notification-slide-down {
    from { opacity: 0; transform: translateY(-16px); }
    to   { opacity: 1; transform: translateY(0); }
}
@keyframes animal-notification-slide-up {
    from { opacity: 1; transform: translateY(0); }
    to   { opacity: 0; transform: translateY(-16px); }
}

/* 减弱动效 */
@media (prefers-reduced-motion: reduce) {
    .placement-top, .placement-bottom,
    .placement-top.leaving, .placement-bottom.leaving {
        animation-duration: 0.01s;
    }
}

Key implementation points:

  • Global store (module-level storeItems)+ useSyncExternalStore; after React components subscribe, re-render is triggered via setState .
  • Single NotificationView internally maintains leaving state: user clicks × or duration expires → set leaving=true → after the 250ms exit animation ends → callback onRemove deletes from the store and triggers item.onClose。
  • When explicitly key ,open will first findIndex the existing key; if it exists, replace the item in the store (using next[idx] = item), otherwise append. Suitable for streaming update scenarios such as upload progress.
  • Note: after the user clicks × to close, the key has already disappeared from the store, and the next open with the same key will be treated as "new" and pop up again. The caller needs to use a closure dismissed flag to suppress the scenario of "the user has already closed it but it gets popped back".Key point:dismissed must be set to trueat the "instant of clicking ×", and cannot be placed only in onClose —— onClose has to wait for the 250ms exit animation to finish, during which the same key open will still revive the notification in the leaving state by updating it in place. The correct approach: set it synchronously in closeIcon 's onClick (before the parent button's handleCloseClick), then set it again inonClose as a fallback (refer to demo/components/Notification/index.tsx)。
  • destroy() also triggers synchronously onClose: consistent with the "click × / duration expiry" contract; the removed item's onClose is called immediately (no 250ms exit animation). This way, the closure flag set in onClose can take effect uniformly across all close paths —— after the user clicks "destroy close all", subsequent queued open calls with the same key will also be dismissed intercepted.
  • Notification(config) called directly is equivalent to Notification.open(config)(type='info'). All methods share the open path, only the type differs.
  • Note on type exports:Notification is a value (API function object),NotificationStatic is its type; the project barrel src/index.ts exports them separately to avoid a name conflict with the browser global Notification constructor.
  • The close button's click internal stopPropagationdoes not bubble to the notification body to trigger onClick。
  • When onClick is provided, the entire <div> is promoted to role="button" tabIndex={0}, supporting Enter / Space keyboard triggering.

Full Props signature:

type NotificationType = 'success' | 'info' | 'warning' | 'error';
type NotificationPosition = 'top' | 'topLeft' | 'topRight' | 'bottom' | 'bottomLeft' | 'bottomRight';
type NotificationPlacement = 'top' | 'bottom';

interface NotificationConfig {
    message: ReactNode;              // 必填
    description?: ReactNode;
    duration?: number;               // 默认 4.5 秒,传 0 关闭自动关闭
    position?: NotificationPosition; // 默认 'top'(顶部居中)
    type?: NotificationType;
    icon?: ReactNode;                // 覆盖 type 默认图标
    btn?: ReactNode;                 // 自定义操作按钮
    key?: string;                    // 同 key 二次调用会更新现有通知
    onClose?: () => void;            // 退场动画结束触发
    onClick?: () => void;            // 点击通知本体触发
    closeIcon?: ReactNode;
    className?: string;
    style?: CSSProperties;
}

Progress

Source:src/components/Progress/Progress.tsx(controlled rendering + aria adaptation) + types.ts(type definitions) + progress.module.less。 JSX component(non-imperative):percent passed in as controlled, smoothly animates from 0 to the target value. The track is a sandy-colored pill with an inner shadow, and the fill directly reuses Button loading's -45° stripes (#0ec4b6 / #01b0a7), scrolling infinitely from right to left (1s linear), visually "the same in-progress style" as Button.

props:

type ProgressSize = 'small' | 'middle' | 'large';
type ProgressInfoPosition = 'inside' | 'right' | 'top';

interface ProgressProps {
    percent: number;            // 必填,0-100,自动 clamp;非整数对 aria 四舍五入
    size?: ProgressSize;        // small=12px / middle=20px / large=28px
    showInfo?: boolean;         // 默认 true
    infoPosition?: ProgressInfoPosition; // 默认 'inside'
    infoFormat?: (p: number) => ReactNode; // 默认 `${p}%`
    duration?: number;          // 秒;0 关闭 fill 宽度动画;默认 0.6(不影响斜纹滚动)
    className?: string;
    style?: CSSProperties;
}

Track (exact value):

.track {
    position: relative;
    flex: 1 1 auto;
    width: 100%;
    min-width: 80px;
    background: #f8f8f0;          /* 主背景色 (与 --animal-bg 一致, 视觉融入页面) */
    border: 2px solid #e8dcc8;     /* 极浅描边, 比 #c4b89e 浅一档, 整体更柔 */
    box-shadow: inset 0 2px 4px rgba(114, 93, 66, 0.08); /* 内凹陷(很弱) */
    border-radius: 999px;         /* pill */
    overflow: hidden;
}
.track.size-small  { height: 12px; border-width: 1.5px; }
.track.size-middle { height: 20px; }
.track.size-large  { height: 28px; }

Fill (exact value, 1:1 with Button loading):

.fill {
    position: absolute;
    top: 0; left: 0; bottom: 0;
    width: 0;
    border-radius: 999px;
    background: #0ec4b6;
    background-image: repeating-linear-gradient(
        -45deg,
        #0ec4b6 0, #0ec4b6 10px,
        #01b0a7 10px, #01b0a7 20px
    );
    background-size: 28.28px 28.28px;  /* 10px * √2,与 Button loading 一致 */
    animation: animal-progress-stripe 1s linear infinite;
    transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1);
    overflow: hidden;
    display: flex; align-items: center; justify-content: flex-end; padding-right: 4px;
}
@keyframes animal-progress-stripe {
    0%   { background-position: 0 0; }
    100% { background-position: -28.28px 0; }
}

Info text:

.infoInside {
    position: absolute; right: 8px; top: 50%; transform: translateY(-50%);
    color: #fff; font-weight: 800; font-size: 11px;  /* small 9px / large 13px */
    letter-spacing: 0.02em; text-shadow: 0 1px 1px rgba(0, 0, 0, 0.15);
    pointer-events: none; white-space: nowrap; z-index: 1;
}
.info.right { min-width: 44px; text-align: right; color: #725d42; font-weight: 700; }
.info.top   { align-self: flex-end; color: #725d42; font-weight: 700; }

Key interaction details:

  • infoPosition="inside" + percent < 18% , the text automatically moves from inside the fill (white) to the end of the track (dark #725d42), avoiding white text being unreadable on the sandy-colored track. This is the only "magic" behavior; everything else is declarative.
  • duration=0 → disable the fill width transition (transition: none), snapping into place instantly;does not affect stripe scrolling。
  • Stripe scrolling is linked with prefers-reduced-motion: reduce : when reduced motion is preferred, animation: none, but the fill width can still transition (also set to none).
  • The old status / strokeColor / leafAnimated has been fully removed: the fill color is fixed to the same teal stripes as Button loading, and only one "in-progress" visual is kept in the library to avoid clashing with status colors.
  • a11y: the root div has role="progressbar" + aria-valuemin=0/aria-valuemax=100/aria-valuenow=<四舍五入后的 percent>/aria-valuetext=<infoFormat 的字符串结果>。
  • prefers-reduced-motion: reduce all animations are automatically disabled.

BackTop

Source code:src/components/BackTop/BackTop.tsx + back-top.module.less。

A fixed back-to-top button in the bottom-right corner, using the Nook bag PNG by default (base64 embedded); clicking it smoothly scrolls to the top with easeInOutQuad.

props:

interface BackTopProps {
    target?: () => HTMLElement | Window; // 默认 () => window
    visibilityHeight?: number;           // 默认 400
    duration?: number;                   // 动画时长 ms, 默认 300
    icon?: ReactNode;                    // 自定义图标
    onClick?: (e: MouseEvent) => void;
    className?: string;
    style?: CSSProperties;
}

Exact style values:

// 容器
position: fixed;
bottom: 48px;
right: 32px;
z-index: 1000;
cursor: pointer;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s, transform 0.3s, visibility 0.3s cubic-bezier(0.4,0,0.2,1);

// visible 状态
opacity: 1;
visibility: visible;
transform: translateY(0);

// 图标
width: 120px;           // 桌面端
height: 120px;
object-fit: contain;    // 保持原图 158×136 比例不拉伸
filter: drop-shadow(0 4px 10px rgba(91,78,30,0.22));

// hover
transform: scale(1.08);
filter: drop-shadow(0 4px 14px rgba(91,78,30,0.32));

// focus-visible
outline: 2px solid #ffcc00;
outline-offset: 4px;
border-radius: 50%;

// 移动端 @media (max-width: 768px)
bottom: 24px;
right: 16px;
图标 80×80px;

Key interaction details:

  • By default listens to window.scroll, shows after visibilityHeight .
  • target The prop supports passing in a custom scroll container function.
  • The scroll animation uses requestAnimationFrame + easeInOutQuad easing.
  • Enter/Space on the keyboard triggers scrolling.
  • The icon is a 158×136px PNG, scaled proportionally through object-fit: contain within a 120×120px container.

Skeleton

Source code:src/components/Skeleton/Skeleton.tsx + skeleton.module.less。

Skeleton loading placeholder component. Four variants:text / circle / rect / paragraph。loading=false renders children directly.

props:

type SkeletonVariant = 'text' | 'circle' | 'rect' | 'paragraph';

interface SkeletonProps {
    loading?: boolean;           // 默认 true
    variant?: SkeletonVariant;   // 默认 'text'
    active?: boolean;            // 流光动画, 默认 true
    rows?: number;               // paragraph 行数, 默认 3
    width?: number | string;     // text/circle/rect 宽度
    rowWidths?: (number | string)[]; // paragraph 行宽数组
    widthValue?: number | string;    // circle/rect 宽
    heightValue?: number | string;   // circle/rect 高
    className?: string;
    style?: CSSProperties;
    children?: ReactNode;
}

// 子组件
SkeletonButtonProps { size?: 'small'|'middle'|'large'; active?: boolean; }
SkeletonInputProps  { size?: 'small'|'middle'|'large'; active?: boolean; }
SkeletonAvatarProps { size?: 'small'|'middle'|'large'; shape?: 'circle'|'square'; active?: boolean; }

Exact style values:

// 底色
@bg-base: #eae5db;      // 浅米灰
@bg-line: #dfd9ce;      // 行底色稍深

// 流光(暖白)
@shimmer-light: rgba(255, 252, 242, 0.55);
@shimmer-mid: rgba(255, 250, 235, 0.18);

// 通用
.skeleton {
    background: @bg-base;
    border-radius: 12px;            // 最小圆角
    overflow: hidden;
    position: relative;
}

// 流光动画
.active::after {
    background: linear-gradient(90deg, transparent, @shimmer-mid, @shimmer-light, @shimmer-mid, transparent);
    animation: animal-skeleton-shimmer 1.6s ease-in-out infinite;
}

// 变体圆角
.vt-text   { border-radius: 12px; height: 16px; }
.vt-circle { border-radius: 50%; }
.vt-rect   { border-radius: 18px; }
.vt-paragraph { background: none; }
.line      { border-radius: 12px; background: @bg-line; }

// 子组件
.skeleton-btn   { border-radius: 50px; }             // pill 状
.skeleton-input { border-radius: 50px; }             // pill 状
.skeleton-avatar { border-radius: (circle)50%/(square)12px; }

Key interaction details:

  • The shimmer animation is a warm white gradient that sweeps from left to right, lasting 1.6s.
  • All corner radii are ≥12px, in line with the "no sharp corners" rule.
  • In paragraph mode the last line defaults to 60% width (can be overridden with rowWidths ).
  • aria-hidden blocks screen readers.

3. Demo layout exact specification

These are the actual layout values of the Demo site (demo/App.tsx), used to reproduce the complete page effect:

Overall layout

/* 页面背景 */
/* 首页 */
background:
    url(home_bg.svg) center/cover no-repeat,
    #7dc395;
/* 组件页 */
background: url(content_bg_pc.jpg) center fixed;

/* Sidebar */
width: 220px;
min-width: 220px;
background: url(menu_bg.svg) center/cover no-repeat;

Sidebar exact values

/* 顶部 Logo 区 */
padding: 20px 16px 12px;
border-bottom: 1px solid #e8e2d6;
font-weight: 700;
font-size: 15px;
color: #725d42;
letter-spacing: -0.3px;

/* Logo 图片 */
width: 24px;
height: 24px;
margin-right: 8px;

/* 菜单列表 */
padding: 8px 0;

/* 分类标题 */
padding: 12px 16px 4px;
font-size: 11px;
color: #a0936e;
font-weight: 600;
letter-spacing: 0.5px;
text-transform: uppercase;

/* 菜单项 */
margin: 1px 5px;
height: 40px;
padding: 0 16px;
padding-left: 26px;
font-weight: 600;
font-size: 14px;
border-radius: 12px;
transition: all 0.15s;

/* inactive */
color: #8a7b66;
background: transparent;
/* inactive hover */
background: #d6dff0;
/* active */
color: #fff;
background: #b7c6e5;

Main content area

/* 桌面 */
padding: 32px 40px;

/* 底部装饰图(桌面端,固定定位)*/
left: 220px;
width: calc(100% - 220px);
z-index: 0;
pointer-events: none;

Mobile adaptation

/* 顶栏 */
height: 52px; padding: 0 12px;
background: rgba(255, 252, 244, 0.92);
backdrop-filter: blur(8px);
border-bottom: 1px solid #e8e2d6;
z-index: 50;

/* 按钮 */ font-size: 20px; color: #725d42; padding: 4px 8px; border-radius: 8px;

/* 主内容区 padding-top */ 68px;

/* 抽屉 */
width: 240px; z-index: 99;
box-shadow: 4px 0 24px rgba(0, 0, 0, 0.15);
/* 遮罩 */ background: rgba(0, 0, 0, 0.35); z-index: 98;

4. HomePage exact specification

/* Hero 区域 */
padding: 60px 40px 40px;
min-height: 80vh;

/* 主标题 */
font-size: 50px;
font-weight: 700;
color: #fff9e6;
text-shadow: 0px 4px 1px rgba(0, 0, 0, 0.4);
margin: 0 0 12px;

/* 版本 Badge */
font-size: 12px;
font-weight: 600;
padding: 2px 10px;
border-radius: 10px;
background: #e6f9f6;
color: #19c8b9;
margin-left: 8px;

/* 副标题 */
font-size: 17px;
color: #7c5734;
line-height: 1.7;
margin: 0 0 28px;
max-width: 520px;

/* Logo 图片 */
width: 172px;
height: 172px;

/* Section */
padding: 48px 40px;
max-width: 960px;
margin: 0 auto;

/* Section 标题 */
font-size: 24px;
font-weight: 700;
color: #725d42;
margin: 0 0 8px;

/* Section 描述 */
font-size: 14px;
color: #7c5734;
margin-bottom: 32px;

/* Feature 网格 */
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;

/* Feature Card hover */
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(114, 93, 66, 0.15);

/* Feature 图标 hover */
transform: scale(1.1) rotate(-4deg);

/* 代码块 */
max-width: 600px;
margin: 0 auto;
padding: 20px 28px;
background: #2b2118;
border: 1px solid #3d3028;
border-radius: 20px;
font-size: 13px;
font-weight: 600;
color: #e8d5bc;
line-height: 1.8;

Code highlighting colors:

Token type Color
Comment #6b5e50(italic, weight 400)
String #a8d4a0
JSX tag #f0a870
Keyword / npm/pnpm #f0a870
Command verb (install/add) #a8d4a0
Bracket {} #d4b896
Arrow => #d4a0e0
CSS variable name #e8c87a
:root #f0a870
Hexadecimal color value #8ab8e0

5. Complete self-implemented CSS variable template

When not relying on a component library, declare the following variables in :root :

:root {
    /* 字体 */
    --animal-font: Nunito, 'Noto Sans SC', -apple-system, 'PingFang SC', 'Hiragino Sans GB', sans-serif;

    /* 主色 */
    --animal-primary: #19c8b9;
    --animal-primary-hover: #3dd4c6;
    --animal-primary-active: #11a89b;
    --animal-primary-bg: #e6f9f6;

    /* 文字 */
    --animal-text: #794f27;
    --animal-text-body: #725d42;
    --animal-text-secondary: #9f927d;
    --animal-text-muted: #8a7b66;
    --animal-text-disabled: #c4b89e;

    /* 背景 */
    --animal-bg: #f8f8f0;
    --animal-bg-content: rgb(247, 243, 223);
    --animal-bg-disabled: #f0ece2;

    /* 边框 */
    --animal-border: #c4b89e;
    --animal-border-hover: #a89878;

    /* 圆角 */
    --animal-radius-sm: 12px;
    --animal-radius: 18px;
    --animal-radius-lg: 24px;
    --animal-radius-pill: 50px;

    /* 3D 阴影 */
    --animal-shadow-btn: #bdaea0;
    --animal-shadow-input: #d4c9b4;
    --animal-shadow-switch: #5a9e1e;

    /* 游戏特殊色 */
    --animal-focus-yellow: #ffcc00;
    --animal-focus-yellow-d: #e0b800;
    --animal-sidebar-active: #b7c6e5;
    --animal-sidebar-hover: #d6dff0;

    /* 状态 */
    --animal-success: #6fba2c;
    --animal-warning: #f5c31c;
    --animal-error: #e05a5a;

    /* 动效 */
    --animal-ease: cubic-bezier(0.4, 0, 0.2, 1);
    --animal-duration-fast: 0.15s;
    --animal-duration: 0.25s;
    --animal-duration-slow: 0.35s;
}

6. 7 design iron rules

  1. Color: earth brown-toned text + mint teal-green primary color + cream off-white background; pure black / cool gray forbidden
  2. Corner radius: minimum 12px; buttons and input fields must be 50px pill-shaped
  3. Dimensionality: 3D thick shadow (0 Npx 0 0 [暗色] + hover lift / active press-down)Only for primary buttons / danger-primary buttons / Input / Switch; default / dashed / text / link buttons only need a soft elevation shadow (0 2px 4px / 0 3px 10px rgba(61,52,40,...))
  4. Font: Nunito (Google Fonts) rounded typeface, buttons/titles at weight 600+, never use light weights
  5. Motion: transitions 0.15~0.35s, easing cubic-bezier(0.4, 0, 0.2, 1), smooth and not stiff
  6. Focus: input fields use yellow #ffcc00, buttons use teal #19c8b9, never use blue
  7. Forbidden: right-angled rectangular interactive elements, pure black text #000, cool blue tones, flat shadowless design

7. New component file structure template

src/components/MyComponent/
├── MyComponent.tsx          # 组件逻辑(必须设置 displayName)
├── myComponent.module.less  # CSS Modules 样式
└── index.ts                 # 统一导出

src/index.ts Append:

// 方式 A:组件用 default export
export { default as MyComponent } from './components/MyComponent';
export type { MyComponentProps } from './components/MyComponent/MyComponent';

// 方式 B:组件用 named export(如 Checkbox / CodeBlock / Select / Icon / Tabs 当前采用)
export { MyComponent } from './components/MyComponent';
export type { MyComponentProps } from './components/MyComponent';

Two styles coexist in the repository; for new components, just choose one, as long as src/index.ts can be successfully re-exported.

Less template (directly using design tokens):

@import '../../styles/variables.less';

.container {
    background: @bg-color-content; // rgb(247,243,223)
    color: @text-color-body; // #725d42
    border: @border-width solid @border-color-light; // 2px solid #c4b89e
    border-radius: @border-radius-base; // 18px
    font-family: @font-family;
    font-weight: 500;
    letter-spacing: 0.01em;
    transition: all @motion-duration-base @motion-ease;
    box-shadow: 0 3px 0 0 @shadow-input; // #d4c9b4

    &:hover:not(.disabled) {
        border-color: @border-color-hover; // #a89878
        transform: translateY(-1px);
        box-shadow: 0 4px 0 0 @shadow-input;
    }

    &:focus-within {
        border-color: @focus-yellow; // #ffcc00
        box-shadow:
            0 3px 0 0 @focus-yellow-dark,
            0 0 0 3px rgba(255, 204, 0, 0.15);
    }

    &.disabled {
        opacity: 0.6;
        cursor: not-allowed;
        background: @bg-color-disabled;
        color: @text-color-disabled;
        border-color: @shadow-input;
        box-shadow: none;
    }
}

TSX template:

import React from 'react';
import styles from './myComponent.module.less';
import classNames from 'classnames';

export interface MyComponentProps {
    /** 尺寸 */
    size?: 'small' | 'middle' | 'large';
    /** 禁用 */
    disabled?: boolean;
    /** 子元素 */
    children?: React.ReactNode;
    /** 自定义类名 */
    className?: string;
    /** 自定义样式 */
    style?: React.CSSProperties;
}

const MyComponent: React.FC<MyComponentProps> = ({ size = 'middle', disabled = false, children, className, style }) => {
    const cls = classNames(styles.container, styles[size], { [styles.disabled]: disabled }, className);

    return (
        <div className={cls} style={style}>
            {children}
        </div>
    );
};

MyComponent.displayName = 'MyComponent';
export default MyComponent;

8. Demo Page Specification

Each component is created in demo/components/<ComponentName>/index.tsx create a demo page:

import { CodeBlock, ApiTable } from '../../tools';

const props = [{ name: 'size', type: "'small' | 'middle' | 'large'", default: "'middle'", description: '尺寸' }];

export default function MyComponentDemo() {
    return (
        <div>
            <h2>MyComponent</h2>
            <CodeBlock code={`<MyComponent size="large">内容</MyComponent>`} />
            <ApiTable data={props} />
        </div>
    );
}

and in demo/ComponentPage.tsx register the route, and also write title / desc into demo/pageInfo.ts:

// demo/pageInfo.ts — 供 App 静态导入的轻量元信息
export const PAGE_INFO: Record<string, { title: string; desc: string }> = {
    button: { title: 'Button 按钮', desc: '...' },
    input: { title: 'Input 输入框', desc: '...' },
    switch: { title: 'Switch 开关', desc: '...' },
    card: { title: 'Card 卡片', desc: '...' },
    collapse: { title: 'Collapse 折叠面板', desc: '...' },
    cursor: { title: 'Cursor 光标', desc: '...' },
    time: { title: 'Time 时间', desc: '...' },
    phone: { title: 'Phone 手机', desc: '...' },
    footer: { title: 'Footer 底部装饰', desc: '...' },
    modal: { title: 'Modal 弹窗', desc: '...' },
    typewriter: { title: 'Typewriter 打字机', desc: '...' },
    'divider-comp': { title: 'Divider 分割线', desc: '...' },
    icon: { title: 'Icon 图标', desc: '...' },
    select: { title: 'Select 选择器', desc: '...' },
    checkbox: { title: 'Checkbox 多选框', desc: '...' },
    radio: { title: 'Radio 单选框', desc: '...' },
    tooltip: { title: 'Tooltip 文字提示', desc: '...' },
    tabs: { title: 'Tabs 标签页', desc: '...' },
    title: { title: 'Title 章节标题', desc: '...' },
    loading: { title: 'Loading 加载', desc: '...' },
    table: { title: 'Table 表格', desc: '...' },
    form: { title: 'Form 表单', desc: '...' },
    wallet: { title: 'Wallet 钱袋', desc: '...' },
    codeblock: { title: 'CodeBlock 代码高亮', desc: '...' },
    notification: { title: 'Notification 通知', desc: '...' },
};

New components must have corresponding entries appended, otherwise they will not be displayed in the Demo sidebar.


9. New Component Checklist

  • Google Fonts already in index.html or style entry imported (Nunito + Noto Sans SC)
  • Props interface exported from the component file
  • All props have JSDoc comments (Chinese OK)
  • Stateful components support both controlled (value) and uncontrolled (defaultValue) modes
  • disabled State: cursor: not-allowed + opacity 0.5~0.6 + remove shadow
  • Colors preferentially reference variables.less token, avoid hardcoded hex
  • Shadows use warm tones (#bdaea0 / #d4c9b4 / rgba(61,52,40,...)), not cold black
  • on hover translateY(-1px 或 -4px) + shadow deepens
  • on active translateY(2px) + shadow reduces
  • Focus: for input types use #ffcc00, for button types use #19c8b9
  • Animation uses @motion-duration-* and @motion-ease token
  • Components from src/index.ts export
  • Demo page created in demo/components/
  • Demo in demo/ComponentPage.tsx registered
  • demo/pageInfo.ts append { title, desc } metadata