SuperAPI AI Directory

animal-island-vue-style

Frontend & websites

View repository

Updated 10-15 17:17

About this skill

Animal Island Vue Design Style Guide

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 + optional types.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.less design 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:modelValue and other events)
  • In React, the ReactNode props 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 by item.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/* in src/index.ts to 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 @import Zen Maru Gothic or a similar font and append it to the end of font-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 0 this pixel-level 3D thick shadow;default / dashed / text / link uses 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 #bdaea0 to 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

⚠️ shadow prop defaults to false: no shadow by default; the box-shadow in 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.png Hotspot coordinates (4, 0)
  • Use !important to override the default cursor
  • ⚠️ This componentcannot use scoped: scoped selectors cannot penetrate slot content; it must be registered as global CSS (<style> without scoped, 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, using cloneVNode to preserve the original nodes and styles
  • watch([() => total.value, () => props.speed, () => props.trigger, () => props.autoPlay]), internally setInterval increments step by step count
  • 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 language prop; 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: false off).


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 (-45deg mint 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-face needs to be inserted as a <style> child node into the screenshot root node — see the src/components/WeddingInvitation/WeddingInvitation.vue logic of exportAsImage in embedFontStyles (exposed via defineExpose for 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

  1. Color: Earthy brown-toned text + mint teal-green primary + cream off-white background; pure black / cool gray prohibited
  2. Corner radius: minimum 12px; buttons and input fields must be 50px pill-shaped
  3. 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
  4. Font: Nunito (Google Fonts) rounded font; buttons/headings weight 600+; never use light weights
  5. Motion: transition 0.15~0.35s, easing cubic-bezier(0.4, 0, 0.2, 1), smooth and not stiff
  6. Focus: input fields use yellow #ffcc00, buttons use teal-green #19c8b9, never use blue
  7. 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 @xxx syntax.
  • 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 .bar selectors; 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=&quot;large&quot;>内容</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 + optional types.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/* in src/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 inline defineProps<{...}>(), do not additionally define a named interface 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:expanded convention (modelValue + update:modelValue), while also supporting defaultValue uncontrolled initial value
  • On the React side, ReactNode input params → on the Vue side, change to named slots (#icon / #prefix / #suffix / #footer etc.), use the default slot for structurable content
  • disabled State: cursor: not-allowed + opacity 0.5~0.6 + remove shadow
  • Colors should preferentially reference variables.less token(@xxx) or :root on the var(--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-ease token
  • Components are exported from src/index.ts export
  • Demo page created on demo/pages/<Name>Demo.vue
  • Demo is in demo/ComponentPage.vue or demo/router.ts register in
  • demo/pageInfo.ts append { title, desc } metadata
  • sync update PROMPT.md、AI_USAGE.md、DESIGN_PROMPT.md、skill/SKILL.md four documents
  • npm run build pass vue-tsc --noEmit type checking + vite build + vite-plugin-dts declaration file generation