Division of labor among the three documents(Consult as needed when generating code / adjusting styles, to avoid cross-referencing):
AI_USAGE.md— API manual: each component's props, types, default values, valid values, and disallowed usage.Look 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 templates for developing new components.Look here when you need to implement/extend styles yourself。DESIGN_PROMPT.md— prompt package for external tools (v0 / Figma AI / Midjourney / DALL-E), including clip-path, color palette quick reference, and disallowed list.Only use when feeding other AI。
Overview
animal-island-vue is a Vue 3 + TypeScript UI component library inspired by Animal Crossing: New Horizons. Core of the design language:Warm earth-tone palette + large rounded pill shapes + game button three-dimensionality + soft motion + coexistence of geometric / organic shapes(Geometric example: the swallowtail clip-path of the Title ribbon; organic example: the SVG blob of Modal, the irregular dashed border of WeddingInvitation).
- Source code:
src/components/<ComponentName>/(each component includes*.vue+index.ts+ optionaltypes.ts) - Demo site:
demo/pages/<ComponentName>Demo.vue - Build: Vite (library mode) +
vite.config.ts(library) /vite.config.docs.ts(Demo) - Style system:scoped
<style lang="less" scoped>+ BEM +src/styles/variables.lessdesign tokens (does not use CSS Modules)
Full export list (43 named exports — including subcomponents, imperative APIs, and accompanying exports)
From src/index.ts exports:
| Component | Responsibility | Interactive | Decorative / purely presentational |
|---|---|---|---|
BackTop |
Back-to-top button, Nook bag icon floating window, supports custom scroll container, animation duration, and visible height | ✓ | |
Button |
Button, 5 types × 3 sizes | ✓ | |
Input |
Input, 3 sizes + clear/prefix/suffix | ✓ | |
Switch |
Switch, default/small | ✓ | |
Modal |
SVG blob clipped modal | ✓ | |
Card |
Container,default/dashed, 13 NookPhone solid colors + 13 pattern polka-dot wallpapers (CSS radial-gradient, not images) |
✓ | |
Title |
Section title, ribbon banner (swallowtail clip-path swallowtail + folded-corner shadow + slightly perspective front), 13 color schemes (replacing the old Card type="title") |
✓ | |
Collapse |
Accordion (animation implemented with CSS Grid 0fr↔1fr, no JS animation) | ✓ | |
Select |
Dropdown selector (controlled) | ✓ | |
Skeleton |
Loading placeholder skeleton screens (SkeletonButton, SkeletonInput, SkeletonAvatar) | ✓ | |
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 icons) | ✓ | |
Time |
HUD real-time clock | ✓ | |
Phone |
NookPhone 3×3 app grid | ✓ | |
Footer |
Bottom decorative graphic (sea/tree) |
✓ | |
Divider |
Decorative divider, 5 styles | ✓ | |
Cursor |
Game Finger Cursor Wrapper | ✓ | |
Typewriter |
Typewriter effect, preserving VNode structure | ✓ | |
Tabs |
Tab switching, with optional leaf sway animation | ✓ | |
CodeBlock |
JSX/TS syntax-highlighted code block | ✓ | |
Loading |
Fullscreen overlay + SVG spinner (mint #19c8b9,stroke-dasharray animation) |
✓ | |
Table |
Data table with fixed columns, empty state, and loading | ✓ | |
Carousel |
Carousel with autoplay / arrows / dots / keyboard navigation | ✓ | |
Countdown |
Countdown with odometer-style one-way scrolling digits | ✓ | |
WeddingInvitation |
Wedding invitation (including WeddingInvitationExportButton export to PNG — this is the onlynon-component companion export button in the list) |
✓ |
Type exports:BackTopProps、ButtonProps/ButtonType/ButtonSize/ButtonHTMLType、InputProps/InputSize、SwitchProps/SwitchSize、ModalProps、CardProps/CardType/CardColor、TitleProps/TitleSize/TitleColor、FooterProps/FooterType、CollapseProps、CursorProps、TimeProps、PhoneProps、DividerProps/DividerType、TypewriterProps、SelectProps/SelectOption、SkeletonProps/SkeletonVariant、IconProps/IconName、TabsProps/TabItem、CheckboxProps/CheckboxOption/CheckboxSize/CheckboxValue、RadioProps/RadioOption/RadioSize/RadioValue、TooltipProps/TooltipPlacement/TooltipTrigger/TooltipVariant、CodeBlockProps、LoadingProps、TableProps/TableColumn/TableRecord、CarouselProps、CountdownProps/CountdownSize/CountdownVariant、WeddingInvitationProps/WeddingInvitationExpose/WeddingInvitationExportButtonProps. Runtime values:ICON_LIST。
Vue-side conventions:
- Controlled values are uniformly passed through
v-model/v-model:open/v-model:expanded(i.e.modelValue+update:modelValueand other events)- In React, the
ReactNodeprops are changed on the Vue side tonamed slots(#icon、#prefix、#suffix、#footer、#checked、#unchecked、#question、#empty, Table's#cell-{dataIndex}/#header-{dataIndex}, Tabs' dynamic slots named byitem.key, etc.); other structurable content is uniformly handled throughdefault slotcarried (e.g. Card, Collapse answer area, Modal body, Typewriter, etc.)
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, loading 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 |
The Vue version simultaneously uses
@fontsource/*insrc/index.tsto directly import font subsets (nunito/noto-sans-sc/zen-maru-gothic), so library consumers do not need to manually<link>; if you implement it yourself outside the component library, just import as described above with<link>. If you need to extend Japanese characters, add your own@importZen Maru Gothicor a similar font and append it to the end offont-family.
Font weight hierarchy:
- Body content:500
- Button text, headings, menu items:600–700
- Number emphasis (time digits, clock):900
- placeholder / descriptive text:400
Letter spacing:letter-spacing: 0.01em(body) / 0.02em(buttons/headings) / 1.5px(uppercase weekdays)
Using thin weights (weight < 400) or monospaced fonts is prohibited.
Spacing / Border Radius / Borders
间距:xs=4px sm=8px md=12px lg=16px xl=24px
圆角:sm=12px base=18px lg=24px pill=50px(按钮/输入框)
边框:默认 2px solid,输入框 2.5px,大尺寸输入框 3px
Shadows
/* 卡片/容器阴影(暖色调,非冷黑)*/
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 makes the interface too heavy and overly 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);
}
}
2. Precise Component Style Specifications
BackTop
Back-to-top button, with a Nook bag icon floating window, fixed at the bottom right (position: fixed), appearing after scrolling more than visibilityHeight px.
| Property | Description | Default value |
|---|---|---|
visibilityHeight |
How many px scrolled before showing | 400 |
duration |
Scroll animation duration (ms) | 300 |
target |
Scroll container function | () => window |
onClick |
Click callback | - |
className |
Custom class name | - |
style |
Custom style | - |
<BackTop :visibilityHeight="400" />
<BackTop :duration="800" />
<BackTop :target="() => containerRef" :visibilityHeight="200" />
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 |
Exact value for primary button (only primary / danger-primary use the 3D thick shadow):
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 that primary set of
0 5px / 6px / 1px #bdaea0to default / dashed — the whole thing will look too heavy and too cartoonish.
loading diagonal-stripe animation (exact 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 defaults tofalse: no shadow by default; thebox-shadowin the table below only takes effect when<Input shadow />is explicitly enabled. status (error/warning) shadows and the 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 |
Exact 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 (#checked / #unchecked slots):
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
/* 默认 */
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;
/* hover */
transform: translateY(-2px);
/* dashed 类型 */
border: 2px dashed #e8dcc8;
background: rgb(250, 248, 242);
box-shadow: none;
/* 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 */
Legacy
Card type="title"was removed in v0.9.x; for section titles please use the standalone<Title>component (see below).
Title (Ribbon section title)
Replaces the legacy Card type="title"and renders a game-style ribbon banner: swallowtail ends + folded-corner shadows + slightly perspective front body.
Source:src/components/Title/Title.vue(scoped Less, BEM class name animal-title__*)。
/* 默认(绿色配色,可被 .animal-title--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 */
/* .animal-title__text 内层文字 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 鱼尾形 */
.animal-title__back--left {
clip-path: polygon(100% 0%, 100% 100%, 0% 100%, 30% 50%, 0% 0%);
}
.animal-title__back--right {
clip-path: polygon(0% 0%, 100% 0%, 70% 50%, 100% 100%, 0% 100%);
}
width: 1.7em;
height: 1.7em;
bottom: -0.4em;
/* 折角阴影 —— CSS border 三角 */
.animal-title__fold--left {
border-width: 0 0.95em 0.45em 0;
border-color: transparent var(--rk) transparent transparent;
}
.animal-title__fold--right {
border-width: 0 0 0.45em 0.95em;
border-color: transparent transparent transparent var(--rk);
}
/* 正面主体 */
.animal-title__front {
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 .animal-title--color-app-pink / .animal-title--color-purple / .animal-title--color-app-blue / .animal-title--color-app-yellow / .animal-title--color-app-orange / .animal-title--color-app-teal / .animal-title--color-app-green / .animal-title--color-app-red / .animal-title--color-lime-green / .animal-title--color-yellow-green / .animal-title--color-brown / .animal-title--color-warm-peach-pink on the wrapper; each class simultaneously overrides --rf / --rb / --rk / --rt four variables. See details in Title.vue <style scoped> the 13 lines of .animal-title--color-* definitions at the end.
Example:
.animal-title--color-app-yellow {
--rf: #f7cd67;
--rb: #d4a030;
--rk: #8a6010;
--rt: #725d42;
}
.animal-title--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
scoped Less + BEM; class name root .animal-tabs, internally uses __list / __item / __icon / __leaf / __content and other child elements.
/* 外层容器 */
.animal-tabs {
background: rgb(247, 243, 223);
border-radius: 20px;
border: 2px solid #9f927d;
overflow: hidden;
}
/* 标签列表 */
.animal-tabs__list {
display: flex;
gap: 4px;
padding: 12px;
background: rgba(255, 255, 255, 0.6);
border-bottom: 2px solid #c4b89e;
}
/* 标签项 */
.animal-tabs__item {
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 */
.animal-tabs__item:hover {
background: rgba(25, 200, 185, 0.1);
color: #725d42;
}
/* 激活状态 — 实心 teal 胶囊 + 奶油色字 */
.animal-tabs__item.animal-tabs__item--active {
background: #0cc0b5;
color: #fff9e3;
font-weight: 600;
}
.animal-tabs__item--active.animal-tabs__item--shadow {
box-shadow: 0 3px 0 0 #d4c9b4; /* 仅 shadow opt-in 时启用 */
}
/* 标签图标 */
.animal-tabs__icon {
font-size: 10px;
}
/* 激活时图标放大 */
.animal-tabs__item--active .animal-tabs__icon {
transform: scale(1.2);
}
/* 叶子装饰动画 */
.animal-tabs__leaf {
position: absolute;
right: -6px;
top: -3px;
font-size: 12px;
animation: leafWiggle 2s ease-in-out infinite;
}
/* leafAnimation={false} 时追加 .animal-tabs__leaf--static 修饰符去除 animation */
@keyframes leafWiggle {
0%,
100% {
transform: rotate(0deg);
}
25% {
transform: rotate(-10deg);
}
75% {
transform: rotate(10deg);
}
}
/* 内容区 */
.animal-tabs__content {
padding: 24px;
animation: fadeIn 0.25s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
Modal
Complete path d value for the SVG clip-path (accurately reproducing the blob outline):
<template>
<svg style="position: absolute; width: 0; height: 0" aria-hidden="true">
<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>
</template>
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);
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;
.animal-time__weekday → font-size: 11px;
.animal-time__monthday → font-size: 16px;
.animal-time__time / .animal-time__colon → font-size: 32px;
Phone(NookPhone)
Shell (fixed size, non-responsive):
.animal-phone {
width: 527px;
height: 788px;
background: #f8f4e8; /* 奶油米 */
border-radius: 136px; /* 超大圆角,近似胶囊 */
overflow: hidden;
}
.animal-phone__home-screen {
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:
.animal-phone__date {
padding: 0 70px 31px 70px;
text-align: center;
}
.animal-phone__date-header {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 32px;
font-weight: 800;
letter-spacing: 2px;
color: #dddbcc;
}
.animal-phone__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;
}
}
.animal-phone__day-text {
font-size: 48px;
font-weight: 800;
color: #725c4e;
letter-spacing: 2px;
height: 56px;
margin-top: 20px;
}
3×3 app grid:
.animal-phone__apps-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 32px;
padding: 8px;
flex: 1;
align-content: center;
justify-content: center;
}
.animal-phone__app-item {
width: 123px;
height: 123px;
border-radius: 45px; /* 圆角正方形 */
position: relative;
display: flex;
justify-content: center;
align-items: center;
}
.animal-phone__app-item:hover .animal-phone__app-icon {
animation: iconBounce 0.3s ease-in-out forwards;
}
.animal-phone__app-icon {
width: 100%;
height: 100%;
background-repeat: no-repeat;
background-position: center;
background-size: 70% auto;
}
.animal-phone__app-item--offset {
overflow: hidden;
}
.animal-phone__app-icon--offset {
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.vue):
| 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 binds one background-image: url('./img/icon-*.svg'),iconApp special use 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。
Small red dot (new message):
.animal-phone__badge {
position: absolute;
top: 0;
left: 0;
width: 28px;
height: 28px;
border-radius: 50%;
background: #ff544a;
border: 5px solid #f8f4e8; /* 奶油米描边,形成游戏风徽章 */
}
Bottom status icons:
.animal-phone__icon-wifi {
width: 79px;
height: 29px;
background: url('./img/wifi.svg') center/contain no-repeat;
}
.animal-phone__icon-location {
width: 36px;
height: 36px;
background: url('./img/location.svg') center/contain no-repeat;
}
.animal-phone__icon-page {
width: 65px;
height: 32px;
background: url('./img/page.svg') center/contain no-repeat;
}
.animal-phone__page-indicator {
display: flex;
justify-content: center;
align-items: center;
margin-top: 74px;
}
Behavior: internal onMounted + setInterval(1000) updates the time (onUnmounted cleanup),12 小时制 + AM/PM + 零填充分钟, and the colon blinks on a 1s cycle. The component has no business callbacks and is purely presentational.
Footer
<template>
<Footer />
<!-- 默认:森林(tree,高 60px) -->
<Footer type="sea" />
<!-- 海浪(高 80px) -->
</template>
.animal-footer {
width: 100%;
height: 80px;
background: url('./img/footer-sea.svg') center/contain no-repeat;
}
.animal-footer--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", multicolor (coral#EC7175, sea blue#327A93, light blue#98D2E3, deep teal#008077, etc.).tree: webp forest silhouette, placed at the very bottom of the page.
Divider
<template>
<Divider type="line-brown" />
<!-- 默认 -->
<Divider type="line-teal" />
<Divider type="line-white" />
<Divider type="line-yellow" />
<Divider type="wave-yellow" />
</template>
.animal-divider {
width: 100%;
height: 12px;
background: url('./img/divider-line-brown.svg') center/contain no-repeat;
}
.animal-divider--line-teal {
background-image: url('./img/divider-line-teal.svg');
}
.animal-divider--line-white {
background-image: url('./img/divider-line-white.png');
}
.animal-divider--line-yellow {
background-image: url('./img/divider-line-yellow.svg');
}
.animal-divider--wave-yellow {
background-image: url('./img/wave-yellow.svg');
}
Default SVG color value reference:#D8D0C3(beige-brown),viewBox="0 0 297 14"。
Cursor
<template>
<Cursor>
<App />
<!-- 此范围内所有元素变为游戏手指光标 -->
</Cursor>
</template>
The style file is plain global CSS(not scoped; the class name is fixed as animal-cursor, attached to the root <div> above):
.animal-cursor,
.animal-cursor * {
cursor:
url('./cursor-icon.png') 4 0,
auto !important;
}
cursor-icon.pngHotspot coordinates(4, 0)- Use
!importantto override the default cursor - ⚠️ This componentcannot use
scoped: scoped selectors cannot penetrate slot content; it must be registered as global CSS (<style>withoutscoped, or imported via a global style entry)
Typewriter
<template>
<Typewriter :speed="90" :trigger="openCount" auto-play @done="handleDone">
<p>第一行 <strong>加粗</strong></p>
<p>第二行</p>
</Typewriter>
</template>
Props:
| name | type | default | Description |
|---|---|---|---|
| Default slot | Slot |
— | 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 |
— | Replays when the value changes (usually pass the modal open count or an incrementing key) |
autoPlay |
boolean |
true |
false displays everything at once |
@done |
() => void |
— | Playback complete event |
Implementation highlights:
countText(vnode): recursively counts the plain text length of the VNode tree (including children /el?.textContent/ string child nodes)renderTruncated(vnode, state): recursively trims by the remaining character count, usingcloneVNodeto preserve the original nodes and styleswatch([() => total.value, () => props.speed, () => props.trigger, () => props.autoPlay]), internallysetIntervalincrements step by stepcount- No style file, does not wrap any extra DOM (the default slot renders directly), zero impact on layout
Checkbox
Props:
| name | type | default | Description |
|---|---|---|---|
options |
CheckboxOption[] |
— | Required; for each item { label, value, disabled? } |
modelValue |
Array<string | number> |
— | Controlled selected value (with v-model) |
defaultValue |
Array<string | number> |
[] |
Uncontrolled default value |
size |
'small' | 'middle' | 'large' |
'middle' |
Size |
disabled |
boolean |
false |
Disable all items |
direction |
'horizontal' | 'vertical' |
'horizontal' |
Layout direction |
@update:modelValue |
(values) => void |
— | Selected value changes (v-model) |
@change |
(values) => void |
— | Same as above, business callback |
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 string, internally tokenized and highlighted as JSX/TS |
copyable |
boolean |
true |
Whether to show the copy button in the top-right corner |
Emits:copy(code: string) — triggered after a successful copy.
class and non-layout-related :style keys fall on <pre> ;width / min-width / max-width / margin* keys fall on the outer wrapper (inheritAttrs: false manually dispatched). When the copy button is shown and padding / padding-right is not customized,<pre> automatically adds padding-right: 96px to leave room for the button.
Structure (BEM, scoped Less):
<div class="animal-code-block"> <!-- wrapper: relative, min-width 0, margin 1em 0 -->
<pre class="animal-code-block__code"><!-- 高亮内容 -->
<button class="animal-code-block__copy-btn">复制</button>
</div>
The copy logic is the same as the React version: prioritize navigator.clipboard.writeText, and when unavailable fall back to a temporary textarea + document.execCommand('copy')(clean up the temporary node in finally); the button's three-state text is Copy / Copied / Copy failed, auto-resetting after 2s (clear the timer on unmount); on success emit copy。
/* 复制按钮 */
.animal-code-block__copy-btn {
position: absolute; top: 12px; right: 12px;
min-width: 62px; height: 32px; padding: 0 12px;
border: 1px solid rgba(232, 213, 188, 0.3);
border-radius: 50px;
background: rgba(61, 48, 40, 0.94); color: #e8d5bc;
font-size: 12px; font-weight: 700;
}
.animal-code-block__copy-btn:hover { background: #4b3b31; transform: translateY(-1px); }
.animal-code-block__copy-btn:focus-visible {
outline: 2px solid var(--animal-primary-color, #19c8b9);
outline-offset: 2px;
}
pre default theme:
box-sizing: border-box; width: 100%; margin: 0;
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 |
Vue/ref/computed/onMounted/defineProps/defineEmits/PropType/...(keeps the React name for compatibility with cross-framework code snippets) |
| component | #80c0e0 |
Upper camelCase identifiers (component names, type names) |
| func | #61afef |
Lowercase identifier followed by ( |
| prop | #e8c87a |
Identifier followed by =(template props / assignments) |
| jsx | #f0a870 |
<Tag、</Tag、/> |
| operator | #d4b896 |
{}[]();, and `+-*/=<>& |
| default | #e8d5bc |
Other text |
Not supported
languageprop; non-JS/TS code (Python/Shell/SQL) is colored by general rules and may display inaccurately. No line numbers or line wrapping; the copy button is shown by default (copyable: falseoff).
Radio
Source:src/components/Radio/Radio.vue(scoped Less + BEM class names animal-radio__*)。
| 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.vue(scoped Less + BEM)。default and island are two sets ofcompletely 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) */
.animal-tooltip__island-arrow {
width: 14px;
height: 14px;
border-radius: 50%;
filter: drop-shadow(0 4px 14px rgba(121, 79, 39, 0.14));
}
12 placement options:top / top_start / top_end / bottom / bottom_start / bottom_end / left / left_start / left_end / right / right_start / right_end。
Loading (fullscreen overlay)
Source:src/components/Loading/Loading.vue(scoped Less + BEM)。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's inline loading stripes (
-45degmint stripes 28.28px) belong to the Button component; do not confuse them with the<Loading>fullscreen overlay.
Table
Source:src/components/Table/Table.vue(scoped Less + BEM)。The shell has no solid border; row separation is achieved with ::after dashed horizontal lines; hover rows are 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;
WeddingInvitation
Source:src/components/WeddingInvitation/WeddingInvitation.vue + weddingInvitation.less(external Less, for easy sharing with the export button). This is aspecial card: envelope shell + tear-off raffle ticket at the bottom + floating leaf animation, all exposed to the parent component via defineExpose({ exportAsImage(filename?) }) and exported via modern-screenshot。
/* 信封外壳 */
max-width: 420px; /* 不是 600px */
padding: 56px 36px var(--lottery-h, 160px); /* 顶/侧 56px+36px;底部预留抽奖券高度 */
border-radius: 16px;
/* 多层背景:径向渐变 + 图片,不是单色 #FAF6E8 */
background:
radial-gradient(...) /* 多层 spotlight */,
url(envelope-texture.png);
/* 阴影通过 filter 实现(drop-shadow),便于配合不规则裁切 */
filter: drop-shadow(0 10px 24px rgba(61, 52, 40, 0.18));
/* 内描边 */
box-shadow: inset 0 0 0 2px rgba(114, 93, 66, 0.12);
/* 顶层细点纹理 */
&::before {
background: radial-gradient(circle, ... 14px 14px);
opacity: 0.55;
}
/* 内虚线边框(有机不规则圆角) */
&::after {
border: 1.5px dashed rgba(114, 93, 66, 0.35);
border-radius: 22px 20px 24px 22px / 20px 24px 22px 20px;
}
/* 底部抽奖券(160px 撕条)*/
.animal-wedding__lottery {
height: 160px; /* --lottery-h */
padding: 38px 36px 18px;
background: rgb(247, 243, 223);
/* 1.6px 棕色点阵 10×5px */
background-image: radial-gradient(circle, rgba(114, 93, 66, 0.7) 1.6px, transparent 1.6px);
background-size: 10px 5px;
/* 撕痕阴影 */
box-shadow: inset 0 4px 6px -3px rgba(61, 52, 40, 0.18);
/* 圆形冲孔(信封与撕条接缝处) */
--notch: 14px;
}
/* 4 角叶子:drop-shadow + ±25° / ±115° rotation */
.animal-wedding__corner-leaf {
filter: drop-shadow(0 2px 3px rgba(61, 52, 40, 0.15));
}
/* 浮动小装饰 */
@keyframes float {
0%,
100% {
transform: translateY(0) rotate(0deg);
}
50% {
transform: translateY(-6px) rotate(8deg);
}
}
animation: float 4.5s ease-in-out infinite;
/* 多个装饰用错位 delay:0s / 0.6s / 1.2s / 0.3s / 1s */
/* banner 分隔线 */
.animal-wedding__banner-line {
width: 64px;
height: 2px;
background: linear-gradient(to right, #725d42, transparent);
background: linear-gradient(to right, transparent, #725d42, transparent);
}
When exporting PNG, Chromium does not read
document.fonts, so@font-faceneeds to be inserted as a<style>child node into the screenshot root node — see thesrc/components/WeddingInvitation/WeddingInvitation.vuelogic ofexportAsImageinembedFontStyles(exposed viadefineExposefor the parent component ref to call).
Select
Source:src/components/Select/Select.vue(scoped Less + BEM). Controlled selector; the trigger area is not Input-styled.
/* 触发按钮 */
.animal-select__trigger {
background: #fff;
border: 2px solid #e8dcc8;
border-radius: 12px;
padding: 8px 13px;
color: #725d42;
font-weight: 600;
font-size: 14px;
line-height: 1;
cursor: pointer;
transition: all 0.2s ease;
}
.animal-select__trigger:hover {
border-color: #d4c4a8;
background: #fffdf7;
}
.animal-select__trigger--open {
border-color: #19c8b9;
color: #19c8b9;
}
.animal-select__trigger--disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* 下拉面板 */
.animal-select__dropdown {
background: #ffeea0; /* 香蕉黄 */
border-radius: 28px;
padding: 12px 0;
z-index: 100;
opacity: 0;
transition: opacity 0.2s ease;
}
.animal-select__dropdown--open {
opacity: 1;
}
/* 选项 */
.animal-select__option {
display: flex;
align-items: center;
padding: 10px 30px 10px 14px;
font-size: 14px;
font-weight: 500;
color: #725d42;
cursor: pointer;
transition: background 0.15s ease;
}
.animal-select__option:hover {
background: rgba(25, 200, 185, 0.08);
}
.animal-select__option--selected {
font-weight: 700;
}
Props: modelValue(controlled, required),options({ key, label }[], required),placeholder(default 'Please select'),disabled、ariaLabel、ariaLabelledBy. Options iteration uses v-for + :key。
Skeleton
Source:src/components/Skeleton/Skeleton.vue(scoped Less + BEM). Loading placeholder skeleton screen, 4 subcomponents: Skeleton, SkeletonButton, SkeletonInput, SkeletonAvatar.
/* 容器 */
.animal-skeleton {
display: block;
width: 100%;
}
/* 骨架占位块 */
.animal-skeleton__block {
background: #e8e2d6;
border-radius: 8px;
animation: animal-skeleton-pulse 1.5s ease-in-out infinite;
}
@keyframes animal-skeleton-pulse {
0%,
100% {
opacity: 0.5;
}
50% {
opacity: 1;
}
}
/* variant */
.animal-skeleton__block--text {
height: 16px;
border-radius: 8px;
}
.animal-skeleton__block--circle {
border-radius: 50%;
}
.animal-skeleton__block--rect {
border-radius: 8px;
}
.animal-skeleton__block--paragraph {
margin-bottom: 12px;
}
/* SkeletonButton */
.animal-skeleton-btn {
height: 45px;
border-radius: 50px;
}
/* SkeletonInput */
.animal-skeleton-input {
height: 40px;
border-radius: 50px;
}
/* SkeletonAvatar */
.animal-skeleton-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
}
Props: loading(boolean, default true),variant('text' | 'circle' | 'rect' | 'paragraph', default 'text')、active(boolean, default true),rows(number, default 3),width(number|string)、rowWidths((number|string)[])、widthValue(number|string)、heightValue(number|string). Default slot: the content rendered when loading=false.
Icon
Source:src/components/Icon/Icon.vue(scoped Less + BEM). 10 built-in SVG icons, rendered via background-image .
.animal-icon {
display: inline-block;
background-repeat: no-repeat;
background-position: center;
background-size: contain;
}
.animal-icon--bounce:hover {
animation: animal-icon-bounce 0.3s ease-in-out forwards;
}
@keyframes animal-icon-bounce {
0% {
transform: scale(1) rotate(0deg);
}
50% {
transform: scale(1.2) rotate(-5deg);
}
100% {
transform: scale(1.1) rotate(-4deg);
}
}
10 icon names:icon-miles, icon-camera, icon-chat, icon-critterpedia, icon-design, icon-diy, icon-helicopter, icon-map, icon-shopping, icon-variant. Runtime exports ICON_LIST({ name, label }[])。Props: name(IconName, required),size(number|string, default 24),bounce(boolean, default false),src(string — custom image URL, mutually exclusive with name ).
Tag
Source:src/components/Tag/Tag.vue(scoped Less + BEM). Pill-shaped tag, supports 3 variants × 13 colors.
/* 基础 */
.animal-tag {
display: inline-flex;
align-items: center;
gap: 4px;
line-height: 1;
font-weight: 600;
border-radius: 999px;
border: 1.5px solid transparent;
transition: all 0.2s ease;
user-select: none;
white-space: nowrap;
}
/* 尺寸 */
.animal-tag--small {
height: 24px;
padding: 0 10px;
font-size: 12px;
}
.animal-tag--medium {
height: 29px;
padding: 0 12px;
font-size: 13px;
}
.animal-tag--large {
height: 34px;
padding: 0 16px;
font-size: 15px;
}
/* 变体 */
.animal-tag--solid {
background: rgb(247, 243, 223);
color: #8f734f;
border-color: #d4c4a8;
}
.animal-tag--outlined {
background: transparent;
color: #8f734f;
border-color: #c4b89e;
}
.animal-tag--dashed {
background: transparent;
color: #8f734f;
border-style: dashed;
border-color: #c4b89e;
}
/* 彩色(13 色 NookPhone 调色板) */
.animal-tag--colored.animal-tag--solid {
background: var(--animal-tag-color);
border-color: var(--animal-tag-color);
color: #fff;
}
.animal-tag--colored.animal-tag--outlined,
.animal-tag--colored.animal-tag--dashed {
color: var(--animal-tag-color);
border-color: var(--animal-tag-color);
background: transparent;
}
/* 关闭按钮 */
.animal-tag__close {
width: 16px;
height: 16px;
border: none;
border-radius: 50%;
background: rgba(0, 0, 0, 0.08);
color: inherit;
font-size: 14px;
cursor: pointer;
transition: background 0.15s ease;
}
.animal-tag__close:hover {
background: rgba(0, 0, 0, 0.18);
}
/* 可交互态 */
.animal-tag--clickable {
cursor: pointer;
}
.animal-tag--clickable:hover {
transform: translateY(-1px);
box-shadow: 0 2px 6px rgba(61, 52, 40, 0.12);
}
.animal-tag--clickable:focus-visible {
outline: 2px solid #f5c31c;
outline-offset: 2px;
}
/* 禁用 */
.animal-tag--disabled {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
Props: size('small' | 'medium' | 'large', default 'medium')、variant('solid' | 'outlined' | 'dashed', default 'solid')、color(13 colors, default 'default')、closable、disabled。Emits: close. Default slot for tag content.
Progress
Source:src/components/Progress/Progress.vue(scoped Less + BEM). Linear progress bar with striped animated fill.
.animal-progress {
display: flex;
align-items: center;
width: 100%;
line-height: 1;
user-select: none;
}
.animal-progress__row {
display: flex;
align-items: center;
gap: 12px;
width: 100%;
}
.animal-progress__info {
font-weight: 700;
color: #725d42;
white-space: nowrap;
flex-shrink: 0;
letter-spacing: 0.02em;
}
.animal-progress__track {
flex: 1;
min-width: 80px;
background: #f8f8f0;
border: 2px solid #e8dcc8;
box-shadow: inset 0 2px 4px rgba(114, 93, 66, 0.08);
overflow: hidden;
border-radius: 999px;
}
.animal-progress__track--small {
height: 12px;
border-width: 1.5px;
}
.animal-progress__track--middle {
height: 20px;
}
.animal-progress__track--large {
height: 28px;
}
.animal-progress__fill {
position: absolute;
top: 0;
left: 0;
bottom: 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;
animation: animal-progress-stripe 1s linear infinite;
transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}
.animal-progress__fill--no-transition {
transition: none;
}
@keyframes animal-progress-stripe {
0% {
background-position: 0 0;
}
100% {
background-position: -28.28px 0;
}
}
.animal-progress__info-inside {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
color: #fff;
font-weight: 800;
font-size: 11px;
letter-spacing: 0.02em;
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.15);
white-space: nowrap;
z-index: 1;
}
Props: percent(0–100, required),size('small' | 'middle' | 'large', default 'middle')、showInfo(default true),infoPosition('inside' | 'right' | 'top', default 'inside')、infoFormat(fn)、duration(seconds, default 0.6, 0 means no animation). Affected by prefers-reduced-motion: reduce .
Notification
Imperative API notification system. Source:src/components/Notification/(Notification.ts + NotificationContainer.vue + NotificationView.vue)。
/* 固定定位容器 */
.animal-notification-root {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 2000;
}
.animal-notification__position {
position: fixed;
display: flex;
flex-direction: column;
gap: 12px;
pointer-events: none;
max-width: calc(100vw - 32px);
}
.animal-notification__position--top {
top: 20px;
left: 50%;
transform: translateX(-50%);
}
.animal-notification__position--topLeft {
top: 20px;
left: 20px;
}
.animal-notification__position--topRight {
top: 20px;
right: 20px;
}
.animal-notification__position--bottom {
bottom: 20px;
left: 50%;
transform: translateX(-50%);
}
.animal-notification__position--bottomLeft {
bottom: 20px;
left: 20px;
}
.animal-notification__position--bottomRight {
bottom: 20px;
right: 20px;
}
/* 通知卡片 */
.animal-notification {
pointer-events: auto;
display: flex;
align-items: flex-start;
gap: 12px;
padding: 16px 20px;
background: rgb(247, 243, 223);
border-radius: 16px;
border-left: 5px solid #19c8b9;
box-shadow: 0 3px 14px rgba(61, 52, 40, 0.12);
max-width: 400px;
animation: animal-notification-slide-in 0.3s ease;
}
.animal-notification--leaving {
animation: animal-notification-slide-out 0.25s ease forwards;
}
@keyframes animal-notification-slide-in {
from {
opacity: 0;
transform: translateY(-12px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes animal-notification-slide-out {
from {
opacity: 1;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(-12px);
}
}
/* 类型颜色 */
.animal-notification--type-success {
border-left-color: #6fba2c;
}
.animal-notification--type-info {
border-left-color: #19c8b9;
}
.animal-notification--type-warning {
border-left-color: #f5c31c;
}
.animal-notification--type-error {
border-left-color: #e05a5a;
}
/* 图标 */
.animal-notification__icon-wrap {
width: 24px;
height: 24px;
flex-shrink: 0;
font-size: 20px;
}
.animal-notification--type-success .animal-notification__icon-wrap {
color: #6fba2c;
}
.animal-notification--type-info .animal-notification__icon-wrap {
color: #19c8b9;
}
.animal-notification--type-warning .animal-notification__icon-wrap {
color: #f5c31c;
}
.animal-notification--type-error .animal-notification__icon-wrap {
color: #e05a5a;
}
/* 标题/描述 */
.animal-notification__title {
font-size: 14px;
font-weight: 600;
color: #725d42;
line-height: 1.4;
}
.animal-notification__description {
margin-top: 4px;
font-size: 12px;
font-weight: 500;
color: #8a7b66;
line-height: 1.5;
}
/* 关闭按钮 */
.animal-notification__close {
flex-shrink: 0;
width: 20px;
height: 20px;
border: none;
background: transparent;
color: #c4b89e;
font-size: 16px;
cursor: pointer;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s ease;
}
.animal-notification__close:hover {
background: rgba(114, 93, 66, 0.1);
color: #725d42;
}
API:Notification.open(config)、.success()、.info()、.warning()、.error()、.destroy(key?). Config fields:message(required),description、duration(default 4.5s, 0 = no auto-close),position(6 types),icon(VNode)、btn(VNode)、key、onClose、onClick、closeIcon、className、style. Requires placing <NotificationContainer />。
Drawer
Source:src/components/Drawer/Drawer.vue(scoped Less + BEM). Side/top/bottom slide-out panel with focus trap and background depth-of-field effect.
/* 遮罩 */
.animal-drawer__mask {
position: fixed;
inset: 0;
z-index: 999;
background: rgba(0, 0, 0, 0.35);
animation: animal-fade-in 0.25s ease;
}
/* 面板 */
.animal-drawer__panel {
position: fixed;
z-index: 1000;
background: rgb(247, 243, 223);
display: flex;
flex-direction: column;
box-shadow: 0 8px 32px rgba(61, 52, 40, 0.18);
transition: transform 0.3s cubic-bezier(0.2, 0, 0.2, 1);
}
.animal-drawer__panel--right {
top: 0;
right: 0;
bottom: 0;
transform: translateX(0);
}
.animal-drawer__panel--left {
top: 0;
left: 0;
bottom: 0;
transform: translateX(0);
}
.animal-drawer__panel--top {
top: 0;
left: 0;
right: 0;
transform: translateY(0);
}
.animal-drawer__panel--bottom {
bottom: 0;
left: 0;
right: 0;
transform: translateY(0);
}
.animal-drawer__panel--enter {
transform: translateX(0) translateY(0);
}
.animal-drawer__panel--leave {
transform: translateX(100%);
}
/* 头部 */
.animal-drawer__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px 24px 0;
flex-shrink: 0;
}
.animal-drawer__title {
font-size: 20px;
font-weight: 700;
color: #794f27;
}
.animal-drawer__close {
width: 28px;
height: 28px;
border: none;
background: transparent;
font-size: 20px;
color: #c4b89e;
cursor: pointer;
border-radius: 50%;
transition: all 0.15s ease;
}
.animal-drawer__close:hover {
background: rgba(114, 93, 66, 0.1);
color: #725d42;
}
/* 内容区 */
.animal-drawer__body {
flex: 1;
overflow-y: auto;
padding: 20px 24px;
}
.animal-drawer__footer {
flex-shrink: 0;
padding: 16px 24px;
border-top: 1px solid #e8e2d6;
}
Props: open(required),title、placement('left' | 'right' | 'top' | 'bottom', default 'right')、width(default 378),height(default 300),maskClosable(default true),pushBackground(default true),footer、maskStyle。Emits: close. Focus trap: Tab cycling, ESC to close, restore focus after closing. Locks body scroll when open.pushBackground applies scale(0.94) + blur(1px) + borderRadius(14px)。
Wallet
Source:src/components/Wallet/Wallet.vue(scoped Less + BEM). NookPhone-style currency display with money bag + amount pill.
.animal-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);
user-select: none;
line-height: 1;
}
.animal-wallet--small {
--wallet-pill-w: 96px;
--wallet-pill-h: 32px;
--wallet-bag: 38px;
--wallet-text-size: 12px;
--wallet-halo: 3px;
}
.animal-wallet--large {
--wallet-pill-w: 168px;
--wallet-pill-h: 52px;
--wallet-bag: 62px;
--wallet-text-size: 22px;
--wallet-halo: 5px;
}
.animal-wallet__bag-slot {
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
width: var(--wallet-bag);
height: var(--wallet-bag);
z-index: 1;
}
.animal-wallet__bag-img {
width: 100%;
height: 100%;
object-fit: contain;
}
.animal-wallet__pill {
width: var(--wallet-pill-w);
height: var(--wallet-pill-h);
background: #b3a046; /* 橄榄黄 */
border-radius: 999px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 0 0 var(--wallet-halo) #fffbe7;
}
.animal-wallet__value {
color: #fff;
font-size: var(--wallet-text-size);
font-weight: 800;
letter-spacing: 0.02em;
text-shadow: 0 1px 2px rgba(91, 78, 30, 0.55);
}
Props: value(number|string, default '00,000', numbers are automatically formatted with thousands separators),icon(string — custom image URL),size('small' | 'medium' | 'large', default 'medium')、thousandSeparator(string, default ','). Slot #icon replaces the default money bag icon.
Form (form system)
Source:src/components/Form/(Form.vue + FormItem.vue + FormProvider.vue + context.ts + useForm.ts + validators.ts + types.ts). Declarative form + validation, supports nested fields.
Form component:
.animal-form {
font-family: @font-family;
&--horizontal {
.animal-form-item {
display: flex;
align-items: center;
gap: 12px;
}
}
&--vertical {
.animal-form-item {
display: flex;
flex-direction: column;
gap: 4px;
}
}
&--inline {
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: center;
}
}
FormItem styles:
.animal-form-item {
width: 100%;
&__label {
font-size: 14px;
font-weight: 600;
color: #725d42;
white-space: nowrap;
}
&__required {
color: #e05a5a;
margin-left: 2px;
}
&__control {
flex: 1;
}
&__help {
font-size: 12px;
font-weight: 500;
margin-top: 4px;
line-height: 1.4;
}
&__help--error {
color: #e05a5a;
}
&__help--warning {
color: #dba90e;
}
&--has-error .animal-form-item__control :deep(input) {
border-color: #e05a5a;
}
}
Form Props: form(FormInstance)、initialValues、layout('horizontal' | 'vertical' | 'inline', default 'horizontal')、labelAlign、labelCol/wrapperCol({ span, offset })、size('small' | 'middle' | 'large')、disabled、colon(default true),requiredMark(boolean | 'optional')、onFinish、onFinishFailed、onValuesChange、onReset。
FormItem Props: name(NamePath — supports 'user.name' nesting),label、rules(RuleObject[])、required、dependencies、valuePropName(default 'modelValue')、trigger(default 'onUpdate:modelValue')、getValueFromEvent、normalize、hidden、hasFeedback、validateStatus、help、noStyle、labelCol、wrapperCol、colon、requiredMark、layout、initialValue。
RuleObject: required、message、min、max、len、pattern(RegExp)、whitespace、type('string' | 'number' | 'boolean' | 'integer' | 'float' | 'array' | 'object' | 'email' | 'url' | 'date')、validator(async fn → void | string)。
FormInstance methods(useForm() produces):getFieldValue、getFieldsValue、setFieldValue、setFieldsValue、resetFields、validateFields(returns Promise),submit、setFields、isFieldTouched、isFieldValidating、getFieldError、scrollToField。
DatePicker
Source:src/components/DatePicker/DatePicker.vue(scoped Less + BEM,animal-date-picker*). Calendar picker, with the trigger area visually aligned to Input.
/* 触发区 —— 奶油底、胶囊圆角、无边框 */
.animal-date-picker__trigger {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
background: #fffbe7;
border-radius: 50px;
cursor: pointer;
transition: box-shadow 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.animal-date-picker__trigger--small { height: 32px; padding: 0 14px; font-size: 12px; }
.animal-date-picker__trigger--middle { height: 40px; padding: 0 18px; font-size: 14px; }
.animal-date-picker__trigger--large { height: 48px; padding: 0 22px; font-size: 16px; }
.animal-date-picker__trigger:hover { box-shadow: 0 3px 0 0 #c4b89e; }
.animal-date-picker__trigger--open {
box-shadow: 0 3px 0 0 #e0b800, 0 0 0 3px rgba(255, 204, 0, 0.15);
}
.animal-date-picker__trigger--error { box-shadow: 0 3px 0 0 #c94444; }
.animal-date-picker__trigger--warning { box-shadow: 0 3px 0 0 #dba90e; }
/* 弹出面板 */
.animal-date-picker__panel {
position: absolute;
width: 288px;
padding: 16px;
background: #fffdf7;
border: 1.5px solid #e8dcc8;
border-radius: 20px;
box-shadow: 0 6px 18px rgba(61, 52, 40, 0.12);
z-index: 1000;
}
/* 星期表头 + 日期格 */
.animal-date-picker__week-cell {
width: 36px;
text-align: center;
font-size: 12px;
font-weight: 700;
color: #a09080;
}
.animal-date-picker__day-cell {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 32px;
border: none;
background: transparent;
border-radius: 12px;
font-size: 13px;
font-weight: 500;
color: #725d42;
cursor: pointer;
transition: all 0.15s;
}
.animal-date-picker__day-cell--selected { background: #ffb400; color: #fff; font-weight: 700; }
.animal-date-picker__day-cell--today { color: #19c8b9; font-weight: 700; }
.animal-date-picker__day-cell--outside { color: #d4c9b4; }
.animal-date-picker__day-cell--disabled { color: #c4b89e; cursor: not-allowed; }
.animal-date-picker__day-cell--in-range { background: rgba(255, 180, 0, 0.15); }
.animal-date-picker__day-cell--range-start,
.animal-date-picker__day-cell--range-end { background: #ffb400; color: #fff; font-weight: 700; }
/* footer:今天 / 确定 */
.animal-date-picker__confirm-btn {
padding: 6px 16px;
border: none;
background: rgba(114, 93, 66, 0.1);
border-radius: 12px;
color: #8a7b66;
font-size: 12px;
font-weight: 700;
cursor: pointer;
transition: all 0.15s;
}
.animal-date-picker__confirm-btn:hover { background: #8a7b66; color: #fff; }
Props / Emits: see AI_USAGE.md §1.30. Interactions: click outside to close (document mousedown), flip upward when panel viewport is insufficient / align right when right side is insufficient, ESC to close, Enter to confirm, arrow key grid navigation. Mode switching: date (42-cell calendar) / month / year (3×4 grid).
TimePicker
Source:src/components/TimePicker/TimePicker.vue(scoped Less + BEM,animal-time-picker*). Hour/minute/second scrolling column selection.
/* 触发区 —— 与 DatePicker 同款 Input 对齐视觉 */
.animal-time-picker__trigger {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
background: #fffbe7;
border-radius: 50px;
cursor: pointer;
transition: box-shadow 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.animal-time-picker__trigger--small { height: 32px; padding: 0 14px; font-size: 12px; }
.animal-time-picker__trigger--middle { height: 40px; padding: 0 18px; font-size: 14px; }
.animal-time-picker__trigger--large { height: 48px; padding: 0 22px; font-size: 16px; }
.animal-time-picker__trigger--open { box-shadow: 0 3px 0 0 #e0b800, 0 0 0 3px rgba(255, 204, 0, 0.15); }
/* 弹出面板(含秒 248px / 无秒 172px) */
.animal-time-picker__panel {
position: absolute;
width: 248px;
padding: 14px;
background: #fffdf7;
border: 1.5px solid #e8dcc8;
border-radius: 20px;
box-shadow: 0 6px 18px rgba(61, 52, 40, 0.12);
z-index: 1000;
}
.animal-time-picker__panel--no-seconds { width: 172px; }
/* 三列滚动 */
.animal-time-picker__column-list {
display: flex;
flex-direction: column;
gap: 2px;
max-height: 232px;
overflow-y: auto;
padding: 2px;
}
.animal-time-picker__option {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0; /* 禁止 flex 压缩,否则选项被压扁 */
height: 28px;
border: none;
background: transparent;
border-radius: 16px;
font-size: 13px;
font-weight: 500;
color: #725d42;
cursor: pointer;
transition: all 0.15s;
}
.animal-time-picker__option:hover { background: #ffd54f; color: #725d42; }
.animal-time-picker__option--selected { background: #ffb400; color: #fff; font-weight: 700; }
/* footer:此刻 / 确定 */
.animal-time-picker__confirm-btn {
padding: 6px 16px;
border: none;
background: rgba(114, 93, 66, 0.1);
border-radius: 12px;
color: #8a7b66;
font-size: 12px;
font-weight: 700;
cursor: pointer;
transition: all 0.15s;
}
.animal-time-picker__confirm-btn:hover { background: #8a7b66; color: #fff; }
Props / Emits: see AI_USAGE.md §1.31. Interactions: when opened, the selected item scrolls to center (index×38 − clientHeight/2 + 19), click outside to close, flip panel upward / align right, ESC to close, Enter to confirm.
Image
Source:src/components/Image/Image.vue(non-scoped Less + BEM, global styles consistent with Drawer so the Teleport preview popup layer hits). White mat frame image component, supports lazy loading, load failure placeholder, and click to preview large image.
/* 相框 */
.animal-image {
display: inline-flex;
align-items: center;
justify-content: center;
overflow: hidden;
background: #fff;
padding: 12px;
border-radius: 8px;
box-shadow: 0 8px 14px 0 rgba(0, 0, 0, 0.08);
line-height: 0;
transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
.animal-image__img {
width: 100%;
height: 100%;
opacity: 0;
transition: opacity 0.25s ease;
}
.animal-image--loaded .animal-image__img {
opacity: 1;
}
/* 错误占位 */
.animal-image--error {
flex-direction: column;
gap: 8px;
color: #c4b89e;
font-size: 13px;
line-height: 1.5;
}
/* 预览触发(相框升格为 button) */
.animal-image--preview {
cursor: zoom-in;
appearance: none;
}
/* 14 种 Card 底色(无花纹) */
.animal-image--default {
background: rgb(247, 243, 223);
color: #725d42;
}
.animal-image--app-pink {
background: #fde4e8;
color: #a85565;
}
/* ... 其余 12 种同 Card pattern 底色 */
/* 大图预览遮罩(Teleport 到 body) */
.animal-image__mask {
position: fixed;
inset: 0;
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.55);
animation: animal-image-fade-in 0.2s ease;
}
.animal-image__preview-img {
max-width: min(88vw, 1100px);
max-height: 86vh;
border-radius: 20px;
object-fit: contain;
box-shadow: 0 12px 40px rgba(43, 33, 24, 0.55);
}
.animal-image__close-btn {
position: absolute;
top: 12px;
right: 12px;
width: 40px;
height: 40px;
border-radius: 50%;
background: rgba(216, 220, 226, 0.9);
border: 1.5px solid rgba(255, 255, 255, 0.75);
}
Props:src(string, required),alt(string, default '')、width/height(number|string, numbers automatically get px appended),color(ImageColor, default 'white', 14 types of Card background colors),lazy(boolean, default false → native loading="lazy")、preview(boolean, default true). Emits:load(Event)、error(Event)。preview=true the frame renders as <button>(native Enter/Space activation), preview popup layer via <Teleport to="body"> Mount, supports closing via ESC / overlay click / close button, and after closing, focus returns to the trigger element.
Carousel
Source:src/components/Carousel/Carousel.vue. Carousel component; each direct child element of the default slot is one slide; supports autoplay, looping from first to last, arrow and dot switching, and keyboard navigation.
/* 容器(region 语义,可聚焦) */
.animal-carousel {
position: relative; width: 100%;
border-radius: 20px;
font-family: var(--animal-font-family, 'Nunito', 'Noto Sans SC');
}
.animal-carousel:focus-visible {
outline: 2px solid var(--animal-primary-color, #19c8b9);
outline-offset: 3px;
}
/* 视口 */
.animal-carousel__viewport {
position: relative; overflow: hidden;
min-height: 180px;
background: rgb(247, 243, 223);
border-radius: 20px;
}
/* 幻灯片:绝对定位淡入 + 位移,active 相对定位撑高 */
.animal-carousel__slide {
position: absolute; inset: 0;
opacity: 0; visibility: hidden;
transform: translateX(18px);
transition: opacity/transform/visibility 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.animal-carousel__slide--active {
position: relative;
opacity: 1; visibility: visible;
transform: translateX(0);
}
/* 圆形箭头按钮 42×42(移动端 36×36),::before 画 9×9 折角箭头 */
.animal-carousel__arrow {
position: absolute; top: 50%;
width: 42px; height: 42px;
border: 1.5px solid rgba(121, 79, 39, 0.16);
border-radius: 50%;
background: rgba(255, 255, 255, 0.92);
color: var(--animal-text-color, #794f27);
transform: translateY(-50%);
}
.animal-carousel__arrow--prev { left: 14px; } /* ::before rotate(-45deg) */
.animal-carousel__arrow--next { right: 14px; } /* ::before rotate(135deg) */
/* 圆点指示器:白色胶囊容器,30×30 命中区 + ::before 10px 圆点,active 拉宽 24px 变主题色 */
.animal-carousel__dots {
position: absolute; bottom: 14px; left: 50%;
padding: 7px 10px; border-radius: 50px;
background: rgba(255, 255, 255, 0.85);
transform: translateX(-50%);
}
.animal-carousel__dot--active::before {
width: 24px;
background: var(--animal-primary-color, #19c8b9);
}
/* 右上角播放/暂停按钮 */
.animal-carousel__rotation-control {
position: absolute; top: 14px; right: 14px;
min-width: 58px; height: 32px;
border-radius: 50px;
font-size: 12px; font-weight: 700;
}
@media (prefers-reduced-motion: reduce) { /* 所有过渡归零 */ }
Props:modelValue(number, v-model controlled index),defaultActiveIndex(default 0),autoplay(default false),interval(default 3000, actual minimum 1000),loop(default true),showArrows/showDots(default true),pauseOnHover(default true),ariaLabel(default '轮播图'). Emits:update:modelValue(index)、change(index). Keyboard: ArrowLeft / ArrowRight / Home / End. autoplay pauses on hover/focus (playback control button in the top-right corner), a single slide does not render the controller,loop=false boundary arrows are disabled.
Countdown
Source:src/components/Countdown/Countdown.vue(internal child component DigitRoll.vue implements the scrolling digit positions). Odometer-style one-way scrolling countdown,format supports DD / HH / mm / ss tokens, literals are rendered as-is as separators.
/* 容器:default 白底软阴影 / island 米色虚线边框 */
.animal-countdown {
display: inline-flex; align-items: center;
gap: 8px; width: fit-content;
color: var(--animal-text-color, #794f27);
font-weight: 700; border-radius: 20px;
}
.animal-countdown--default {
padding: 12px 18px;
background: var(--animal-bg-color, #fff);
box-shadow: var(--animal-shadow-sm, 0 2px 4px rgba(61, 52, 40, 0.06));
}
.animal-countdown--island {
padding: 13px 20px;
background: rgb(247, 243, 223);
border: 2px dashed #d4c4a8;
}
/* 数字块(Time 同款渐变底,bordered 时加 1.5px #d4c9b4 边框) */
.animal-countdown__unit {
display: inline-flex; gap: 3px;
padding: 3px 8px; border-radius: 12px;
background: linear-gradient(180deg, #fff 0%, #f8f8f0 100%);
}
/* 滚动数字位:0-9 两轮共 20 面数字条,translateY 每面 5% 单向向下滚动 */
.animal-countdown__digit-cell { overflow: hidden; height: 1.2em; }
.animal-countdown__digit-strip {
display: flex; flex-direction: column;
transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1);
will-change: transform;
}
.animal-countdown__digit-face {
height: 1.2em;
color: #8b7355; font-weight: 900;
font-variant-numeric: tabular-nums;
line-height: 1.2; text-align: center;
}
/* 冒号:900 字重 + top -0.08em 光学居中,字号按尺寸 20/26/34px */
.animal-countdown__colon { font-weight: 900; position: relative; top: -0.08em; }
/* 尺寸:small 40px / middle 48px / large 56px;数字位字号 20/26/34px */
.animal-countdown--small { min-height: 40px; }
.animal-countdown--middle { min-height: 48px; }
.animal-countdown--large { min-height: 56px; }
/* 读屏专用文本(.sr-only,滚动数字条 aria-hidden) */
Props:value(number|Date, required),format(default 'HH:mm:ss')、size('small' | 'middle' | 'large', default 'middle')、variant('default' | 'island', default 'default')、bordered(default false). Emits:change(remaining)(remaining milliseconds),finish()(reached zero, only once). Slots:#prefix explanatory content before the countdown. 250ms polling, timer cleared at zero;value changes restart the timer; when DD is included, HH uses hours within the day, otherwise total hours.
Pagination
Source:src/components/Pagination/Pagination.vue. Pagination component (ghost perfect-circle page number cells + dual color scheme), supports controlled/uncontrolled, page size switching, and quick jump;Table the pagination property embeds the same style.
/* 容器:nav 语义 */
.animal-pagination {
display: inline-flex; align-items: center; gap: 2px;
font-family: var(--animal-font-family, 'Nunito', 'Noto Sans SC');
font-size: 14px; color: #725d42;
user-select: none;
}
.animal-pagination--disabled { opacity: 0.6; }
/* 总条数 */
.animal-pagination__total {
margin-right: 10px; font-size: 13px; font-weight: 600;
color: #a09080; white-space: nowrap;
}
/* 页码/前后翻页:32px 透明底正圆,hover 变色(由变体提供),disabled #d4c9b4 */
.animal-pagination__item {
display: inline-flex; align-items: center; justify-content: center;
width: 32px; height: 32px; padding: 0;
border: none; border-radius: 50%; background: transparent;
color: #725d42; font-size: 13px; font-weight: 500;
cursor: pointer; transition: all 0.15s ease;
}
.animal-pagination__item:focus-visible { outline: 2px solid #ffcc00; outline-offset: 1px; }
/* 当前页:白字 700 字重 + 变体底色(orange #ffc107 / teal #19c8b9) */
.animal-pagination__item--active { color: #fff; font-weight: 700; cursor: default; }
.animal-pagination--orange .animal-pagination__item--active { background: #ffc107; }
.animal-pagination--orange .animal-pagination__item--active:hover { background: #ffb400; }
.animal-pagination--orange .animal-pagination__item:hover:not(:disabled):not(.animal-pagination__item--active) {
background: #ffd54f; color: #725d42;
}
.animal-pagination--teal .animal-pagination__item--active { background: #19c8b9; }
.animal-pagination--teal .animal-pagination__item--active:hover { background: #3dd4c6; }
.animal-pagination--teal .animal-pagination__item:hover:not(:disabled):not(.animal-pagination__item--active) {
background: #e6f9f6; color: #19c8b9;
}
/* 省略号 */
.animal-pagination__ellipsis {
display: inline-flex; align-items: center; justify-content: center;
width: 24px; height: 32px; color: #c4b89e; font-weight: 900; letter-spacing: 1px;
}
/* 每页条数切换器:白底 2px #e8dcc8 边框 34px 触发器(同 Select),箭头展开旋转 180° */
.animal-pagination__size-changer { position: relative; display: inline-flex; margin-left: 8px; }
.animal-pagination__size-trigger {
display: inline-flex; align-items: center; justify-content: space-between; gap: 8px;
height: 34px; padding: 0 12px;
border: 2px solid #e8dcc8; border-radius: 12px;
background: #fff; color: #725d42; font-size: 13px; font-weight: 600;
cursor: pointer; transition: all 0.2s ease;
}
.animal-pagination__size-trigger--open .animal-pagination__caret { transform: rotate(180deg); }
/* 弹层:#ffeea0 28px 圆角向上弹出,option 选中金色 pill bar + 手指光标(同 Select) */
.animal-pagination__size-list {
position: absolute; bottom: calc(100% + 8px); left: 0; z-index: 10;
padding: 8px 0; list-style: none;
background: #ffeea0; border-radius: 28px;
box-shadow: 0 6px 18px rgba(61, 52, 40, 0.12);
}
.animal-pagination__size-option--active::after {
content: ''; position: absolute; left: 0; right: 0; top: 56%;
transform: translateY(-50%); height: 14px; margin: 0 20px;
background: #ffcc00; border-radius: 7px; z-index: -1; opacity: 0.3;
}
/* 快速跳转:奶油底胶囊输入框 52×32px,无 border,focus 无强调 */
.animal-pagination__jumper {
display: inline-flex; align-items: center; gap: 6px; margin-left: 8px;
font-size: 13px; font-weight: 500; color: #8a7b66;
}
.animal-pagination__jumper-input {
width: 52px; height: 32px; padding: 0;
border: none; border-radius: 50px; background: #fffbe7;
color: #725d42; font-size: 13px; font-weight: 700;
text-align: center; outline: none; caret-color: #725d42;
}
Props:total(number, required),current(v-model:current, controlled current page),defaultCurrent(default 1),pageSize(v-model:pageSize, controlled page size),defaultPageSize(default 10),showSizeChanger(default false),pageSizeOptions(default [10, 20, 50, 100])、showQuickJumper(default false),showTotal(default false),disabled(default false),variant('orange' | 'teal', default 'orange')。Emits:update:current(page)、update:pageSize(size)、change(page, pageSize)、showSizeChange(current, size). When page count ≤7, all are displayed; when >7, first and last pages + current page ±1 + ellipsis. The size changer popup closes on outside click / Escape; the jumper accepts only digits, jumps on Enter/blur, and clamps out-of-range values to the boundary pages.Table the pagination property is passed an object to enable client-side pagination (total is calculated by Table based on the length of dataSource).
3. Demo layout precise specification
These are the actual layout values of the Demo site (demo/App.vue), 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 precise 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 precise 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 color scheme:
| Token type | Color |
|---|---|
| Comment | #6b5e50(italic, weight 400) |
| String | #a8d4a0 |
| Vue template tags | #f0a870 |
| Keywords / npm/pnpm | #f0a870 |
| Command verbs (install/add) | #a8d4a0 |
Parentheses {} |
#d4b896 |
Arrows => |
#d4a0e0 |
| CSS variable names | #e8c87a |
:root |
#f0a870 |
| Hexadecimal color values | #8ab8e0 |
5. Complete template of self-implemented CSS variables
When not relying on a component library, in :root declare the following variables:
: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: Earthy brown-toned text + mint teal-green primary + cream off-white background; pure black / cool gray prohibited
- Corner radius: minimum 12px; buttons and input fields must be 50px pill-shaped
- Dimensionality: 3D thick shadow (
0 Npx 0 0 [暗色]+ hover float-up / active press-down)only for primary button / danger-primary button / Input / Switch; default / dashed / text / link buttons use a soft elevation shadow (0 2px 4px / 0 3px 10px rgba(61,52,40,...)) is sufficient - Font: Nunito (Google Fonts) rounded font; buttons/headings weight 600+; never use light weights
- Motion: transition 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-green#19c8b9, never use blue - Prohibited: 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.vue # SFC:<script setup lang="ts"> + <template> + <style lang="less" scoped>
├── types.ts # (可选)独立类型文件,跨文件复用泛型时必需
└── index.ts # 统一导出
src/components/MyComponent/index.ts:
export { default as MyComponent } from './MyComponent.vue';
export type { MyComponentProps } from './types';
src/index.ts append:
export { MyComponent } from './components/MyComponent';
export type { MyComponentProps } from './components/MyComponent';
All components in the repository use
<script setup lang="ts">+ scoped Less + BEM; CSS Modules are prohibited (*.module.less)。
Less / BEM template (using design tokens directly):
@import '@/styles/variables.less';
.animal-mycomp {
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(.animal-mycomp--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;
}
&__icon {
display: inline-flex;
align-items: center;
}
}
Vue SFC template:
<script setup lang="ts">
import type { MyComponentSize } from './types';
interface Props {
/** 尺寸 */
size?: MyComponentSize;
/** 禁用 */
disabled?: boolean;
}
withDefaults(defineProps<Props>(), {
size: 'middle',
disabled: false,
});
defineEmits<{ (e: 'click', event: MouseEvent): void }>();
defineSlots<{ default?: () => unknown; icon?: () => unknown }>();
</script>
<template>
<div
class="animal-mycomp"
:class="[`animal-mycomp--${size}`, { 'animal-mycomp--disabled': disabled }]"
@click="$emit('click', $event)"
>
<span v-if="$slots.icon" class="animal-mycomp__icon">
<slot name="icon" />
</span>
<slot />
</div>
</template>
<style lang="less" scoped>
@import '@/styles/variables.less';
.animal-mycomp {
font-family: @font-family;
font-weight: 600;
letter-spacing: 0.02em;
line-height: 1;
background: @bg-color-input;
color: @warm-color-soft;
border: 2px solid @border-color-light;
border-radius: 50px;
box-shadow: 0 3px 0 0 @shadow-soft;
transition: all @motion-duration-base @motion-ease;
&:hover:not(.animal-mycomp--disabled) {
border-color: @border-color-hover;
transform: translateY(-1px);
box-shadow: 0 4px 0 0 @shadow-soft-hover;
}
&:focus-within {
border-color: #ffcc00;
box-shadow:
0 3px 0 0 #e0b800,
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;
box-shadow: none;
}
&--small {
height: 32px;
padding: 0 16px;
font-size: @font-size-sm;
}
&--middle {
height: 45px;
padding: 0 20px;
font-size: @font-size-base;
}
&--large {
height: 48px;
padding: 0 32px;
font-size: @font-size-lg;
}
&__icon {
display: inline-flex;
align-items: center;
}
}
</style>
Less / BEM tips:
- At the top
@import '@/styles/variables.less';import global tokens; within components uniformly use@xxxsyntax.- Use BEM modifiers for state styles (
&--disabled、&--checked) rather than stacking utility classes, for easier reading and overriding.- Always use
&__namefor child elements, avoiding isolated.foo .barselectors; after scoped compilation, BEM naming is sufficient for isolation.- Duration tokens:
@motion-duration-fast(0.15s) /@motion-duration-base(0.25s) /@motion-duration-slow(0.35s), uniformly use@motion-ease。
8. Demo Page Specification
For each component, in demo/pages/<ComponentName>Demo.vue create a demo page:
<script setup lang="ts">
import { MyComponent } from '../../src';
import { CodeBlock, ApiTable } from '../tools';
const props = [{ name: 'size', type: "'small' | 'middle' | 'large'", default: "'middle'", description: '尺寸' }];
</script>
<template>
<div>
<h2>MyComponent</h2>
<MyComponent size="large">内容</MyComponent>
<CodeBlock :code="`<MyComponent size="large">内容</MyComponent>`" />
<ApiTable :data="props" />
</div>
</template>
and in demo/ComponentPage.vue(or demo/router.ts) 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: '...' },
'wedding-invitation': { title: 'WeddingInvitation 邀请函', desc: '...' },
codeblock: { title: 'CodeBlock 代码高亮', desc: '...' },
};
New components must have a corresponding entry appended, otherwise the Demo sidebar will not display them.
9. New Component Checklist
- Create a new folder
src/components/<Name>/, containing<Name>.vue+index.ts+ optionaltypes.ts - SFC uses
<script setup lang="ts">, style block uses<style lang="less" scoped>, class names follow BEM (.animal-foo/.animal-foo--modifier/.animal-foo__elem),CSS Modules prohibited - Google Fonts are already introduced via
@fontsource/*insrc/index.ts(Nunito + Noto Sans SC + Zen Maru Gothic) - Props interface is exported from the component file or
types.ts(when using generics in an SFC, you must use inlinedefineProps<{...}>(), do not additionally define a namedinterface Props, otherwise vite-plugin-dts will trigger a TS4082 error) - All props have JSDoc comments (Chinese is OK)
- Controlled values use
v-model/v-model:open/v-model:expandedconvention (modelValue+update:modelValue), while also supportingdefaultValueuncontrolled initial value - On the React side,
ReactNodeinput params → on the Vue side, change to named slots (#icon/#prefix/#suffix/#footeretc.), use the default slot for structurable content -
disabledState: cursor: not-allowed + opacity 0.5~0.6 + remove shadow - Colors should preferentially reference
variables.lesstoken(@xxx) or:rooton thevar(--animal-*)CSS variables, avoiding hardcoded hex - Shadows use warm tones (
#bdaea0/#d4c9b4/rgba(61,52,40,...)), not cool black - on hover
translateY(-1px 或 -4px)+ deepened shadow - on active
translateY(2px)+ reduced shadow - Focus: use for input types
#ffcc00, use for button types#19c8b9 - Animations use
@motion-duration-*and@motion-easetoken - Components are exported from
src/index.tsexport - Demo page created on
demo/pages/<Name>Demo.vue - Demo is in
demo/ComponentPage.vueordemo/router.tsregister in -
demo/pageInfo.tsappend{ title, desc }metadata - sync update
PROMPT.md、AI_USAGE.md、DESIGN_PROMPT.md、skill/SKILL.mdfour documents -
npm run buildpassvue-tsc --noEmittype checking +vite build+vite-plugin-dtsdeclaration file generation