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.lessdesign 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,@importthat font yourself and append it to the end offont-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 0this pixel-level 3D thick shadow;default/dashed/text/linkuses 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 defaultbox-shadow: 0 5px 0 0 #bdaea0; - ✅
<Button type="primary">adds 3D shadow;type="default"usesbox-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 />addsbox-shadow: 0 3px 0 0 #d4c9b4;;status="error"not controlled byshadow, always rendered
7. Switch has no outer shadow / handle does not float up
- ❌ handle uses
box-shadowfloats up / usesmargin-topcentered - ✅ 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 addingborder: 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 withborder-radiusrectangular Title - ✅ using
<Title>component, clip-path paired withtransform: 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/headings600-700;Time/Title900;placeholder400
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 #bdaea0to 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
⚠️
shadowprop defaultfalse: no shadow by default; the following table'sbox-shadowonly 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.pnghotspot coordinates(4, 0)- uses
!importantto override the default cursor;classNameis attached directly to the root<div>, with a fixed class name ofanimal-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 ReactNoderenderTruncated(node, state): recursively trim by remaining character count,React.cloneElementpreserves the original nodes and stylesuseEffectdepends on[total, speed, trigger, autoPlay], internallysetIntervalincrementscount- 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
languageprop; 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 (
-45degmint 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/#1677ffintentionally does not usesrc/styles/variables.lessthe 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/#e05a5aetc.
FormItemmust be used inside<Form>or<Form.Provider>, otherwise it throwsForm.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):
valueisnumber, insert thousands separatorsthousandSeparator(default,, pass""close);stringdisplayed as-is;undefined/nullshows00,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 anyiconvia theReactNodeprop. Note: internally it uses the hidden<Icon src={bagIcon} />parameter ofsrc(Icon supports bothnamegoing through ICON_LIST, andsrcgoing 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" />。sizedefaults to 16,colordefaults tocurrentColor。srcmode 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, seeAI_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 transparentdefault placeholder, so that switching between outlined/dashed does not cause size jitter due to the border appearing/disappearing.closable× button's clickstopPropagation, does not bubble up to triggeronClick。- provides
onClick, the entire tag is upgraded torole="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*) usesflex-direction: column-reverseso 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 viasetState. - Single
NotificationViewinternally maintainsleavingstate: user clicks × or duration expires → setleaving=true→ after the 250ms exit animation ends → callbackonRemovedeletes from the store and triggersitem.onClose。 - When explicitly
key,openwill firstfindIndexthe existing key; if it exists, replace the item in the store (usingnext[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
openwith the same key will be treated as "new" and pop up again. The caller needs to use a closuredismissedflag to suppress the scenario of "the user has already closed it but it gets popped back".Key point:dismissedmust be set totrueat the "instant of clicking ×", and cannot be placed only inonClose——onClosehas to wait for the 250ms exit animation to finish, during which the same keyopenwill still revive the notification in the leaving state by updating it in place. The correct approach: set it synchronously incloseIcon'sonClick(before the parent button's handleCloseClick), then set it again inonCloseas a fallback (refer todemo/components/Notification/index.tsx)。 destroy()also triggers synchronouslyonClose: consistent with the "click × / duration expiry" contract; the removed item'sonCloseis 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 bedismissedintercepted.Notification(config)called directly is equivalent toNotification.open(config)(type='info'). All methods share theopenpath, only the type differs.- Note on type exports:
Notificationis a value (API function object),NotificationStaticis its type; the project barrelsrc/index.tsexports them separately to avoid a name conflict with the browser globalNotificationconstructor. - The close button's
clickinternalstopPropagationdoes not bubble to the notification body to triggeronClick。 - When
onClickis provided, the entire<div>is promoted torole="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/leafAnimatedhas 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: reduceall 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 aftervisibilityHeight. targetThe 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: containwithin 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-hiddenblocks 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
- Color: earth brown-toned text + mint teal-green primary color + cream off-white background; pure black / cool gray forbidden
- Corner radius: minimum 12px; buttons and input fields must be 50px pill-shaped
- 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,...)) - Font: Nunito (Google Fonts) rounded typeface, buttons/titles at weight 600+, never use light weights
- Motion: transitions 0.15~0.35s, easing
cubic-bezier(0.4, 0, 0.2, 1), smooth and not stiff - Focus: input fields use yellow
#ffcc00, buttons use teal#19c8b9, never use blue - 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.tscan 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.htmlor 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 -
disabledState: cursor: not-allowed + opacity 0.5~0.6 + remove shadow - Colors preferentially reference
variables.lesstoken, 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-easetoken - Components from
src/index.tsexport - Demo page created in
demo/components/ - Demo in
demo/ComponentPage.tsxregistered -
demo/pageInfo.tsappend{ title, desc }metadata