> **For AI assistants**: Read this entire file before writing any frontend code or template. All decisions here are final. When in doubt about styling, spacing, color, or component behavior — the answer is in this document. Do not invent values. --- ## Design Philosophy **Core aesthetic**: Clean, functional, low-friction. We build tools, not experiences. Every UI element must earn its place. **Principles** (in priority order): 1. **Clarity over cleverness** — if you have to explain the UI, it failed 2. **Density over whitespace** — our users are power users; do not waste screen space 3. **Consistency over novelty** — use existing patterns before inventing new ones 4. **Motion is functional** — animate only to communicate state change, never for decoration **Anti-patterns we actively avoid**: - Skeleton loaders for data that loads in < 300ms (use a spinner instead) - Modal dialogs for destructive actions that are easily reversible - Infinite scroll (we use pagination; users need to share URLs to specific pages) - Tooltips on mobile - Full-width buttons on desktop (max-width: 320px for standalone CTAs) - Mixing card and table layouts in the same list view - Auto-submitting forms on change without explicit confirmation - Generic error messages ("Something went wrong" is never acceptable) - `window.alert` — always use our Dialog/Toast components --- ## Design Tokens All colors, spacing, radius, and shadow values must come from these tokens. **Never write raw hex values or arbitrary px values in components.** ### Color System We use CSS custom properties (variables). Define these in `base.css` under `:root`. ```css :root { /* Backgrounds */ --color-bg-base: #ffffff; /* page background */ --color-bg-subtle: #f9fafb; /* card, sidebar, panel backgrounds */ --color-bg-muted: #f3f4f6; /* disabled states, placeholders, table zebra */ /* Text */ --color-text-primary: #111827; /* body text */ --color-text-secondary:#6b7280; /* labels, captions, helper text */ --color-text-disabled: #9ca3af; /* disabled text */ --color-text-inverse: #ffffff; /* text on dark/colored backgrounds */ /* Borders */ --color-border: #e5e7eb; /* default borders */ --color-border-strong: #d1d5db; /* focused, emphasized borders */ /* Brand / Accent — orange as primary action color */ --color-accent: #f97316; /* primary actions, links, active states */ --color-accent-hover: #ea6c0a; /* hover state of accent */ --color-accent-subtle: #fff7ed; /* light tint for accent backgrounds */ /* Semantic */ --color-success: #16a34a; /* confirmations, completed states */ --color-success-bg: #f0fdf4; --color-warning: #d97706; /* non-blocking alerts */ --color-warning-bg: #fffbeb; --color-danger: #dc2626; /* destructive actions, errors */ --color-danger-bg: #fef2f2; --color-info: #2563eb; /* informational, neutral alerts */ --color-info-bg: #eff6ff; } ``` **Mapping to Tailwind**: Configure `tailwind.config.js` to extend colors using these CSS variables so you can write `text-accent`, `bg-bg-subtle`, etc. If that is not yet configured, use the following Tailwind equivalents as a temporary fallback — but never use arbitrary hex: | Token | Tailwind equivalent | |---|---| | `--color-accent` | `text-orange-500` / `bg-orange-500` | | `--color-accent-hover` | `hover:bg-orange-600` | | `--color-text-primary` | `text-gray-900` | | `--color-text-secondary` | `text-gray-500` | | `--color-text-disabled` | `text-gray-400` | | `--color-border` | `border-gray-200` | | `--color-border-strong` | `border-gray-300` | | `--color-bg-subtle` | `bg-gray-50` | | `--color-bg-muted` | `bg-gray-100` | | `--color-danger` | `text-red-600` / `bg-red-600` | | `--color-success` | `text-green-600` | **Rule**: If you find yourself writing `text-gray-500`, stop. Ask: is this a label, a caption, or secondary content? Then use the semantic token. If you find yourself writing `text-gray-400`, that is disabled text. --- ### Spacing Scale We use a **4px base grid**. Only these values are permitted: | px | Tailwind | |---|---| | 4px | `p-1` / `m-1` / `gap-1` | | 8px | `p-2` / `m-2` / `gap-2` | | 12px | `p-3` / `m-3` / `gap-3` | | 16px | `p-4` / `m-4` / `gap-4` | | 24px | `p-6` / `m-6` / `gap-6` | | 32px | `p-8` / `m-8` / `gap-8` | | 48px | `p-12` / `m-12` / `gap-12` | | 64px | `p-16` / `m-16` / `gap-16` | | 96px | `p-24` / `m-24` / `gap-24` | **Never use**: `p-5`, `p-7`, `p-9`, `p-10`, `p-11` — these break the grid. --- ### Border Radius ``` --radius-sm: 4px → rounded-sm (inputs, badges, table cells) --radius-md: 8px → rounded (cards, buttons) ← default --radius-lg: 12px → rounded-xl (modals, drawers, panels) --radius-full: 9999px → rounded-full (avatars, pill badges, toggles) ``` **Rule**: Never mix radius sizes within the same component. A card with `rounded` should not have children with `rounded-xl`. --- ### Elevation / Shadow ``` --shadow-sm → shadow-sm (subtle card lift, focused inputs) --shadow-md → shadow-md (dropdowns, popovers, floating panels) --shadow-lg → shadow-xl (modals, drawers, dialogs) ``` **Rules**: - Never use `drop-shadow` filter — use `box-shadow` (`shadow-*`) only - Never use `shadow-2xl` — `shadow-xl` is the maximum --- ## Typography **Font stack**: - UI: `Inter` (variable weight, loaded via `` from self-hosted or CDN) - Code / monospace data: `JetBrains Mono` (used for code blocks and numeric data columns only) - Never import fonts via Google Fonts `@import` inside CSS — use `` in `` **Type scale** — use only these sizes, no arbitrary values: | Tailwind class | Size | Weight | Line-height | Usage | |---|---|---|---|---| | `text-xs` | 12px | 400 | 1.5 | Labels, badges, metadata, table captions | | `text-sm` | 14px | 400 | 1.5 | Body text, secondary content, form helpers | | `text-base` | 16px | 400 | 1.6 | Primary body text | | `text-lg` | 18px | 500 | 1.4 | Section headings, drawer titles | | `text-xl` | 20px | 600 | 1.3 | Page sub-headings | | `text-2xl` | 24px | 700 | 1.2 | Page titles | | `text-3xl` | 30px | 700 | 1.1 | Hero headings only — never in app UI | **Rules**: - Max 2 font sizes per component - `font-medium` (500) is the minimum weight for anything interactive (buttons, links, tab labels) - Never use `text-3xl` in application views — only marketing/landing pages - Body text line length: max 72 characters (`max-w-prose`) - Numbers in tables: use `font-mono` (JetBrains Mono) and `tabular-nums` --- ## Page Layout ### App Shell The standard application layout is a **fixed sidebar + scrollable main content** pattern. ``` ┌─────────────────────────────────────────────────────┐ │ Top Nav Bar (h-14, fixed, z-30) │ ├──────────────┬──────────────────────────────────────┤ │ │ Page Header (breadcrumb + actions) │ │ Sidebar ├──────────────────────────────────────┤ │ (w-56, │ │ │ fixed, │ Main Content Area │ │ z-20) │ (overflow-y-auto, flex-1) │ │ │ │ │ │ │ └──────────────┴──────────────────────────────────────┘ ``` - **Top Nav**: `h-14`, `bg-white`, `border-b border-gray-200`, `fixed top-0 inset-x-0 z-30` - **Sidebar**: `w-56`, `bg-gray-50`, `border-r border-gray-200`, `fixed left-0 top-14 bottom-0 z-20`, `overflow-y-auto` - **Main**: `ml-56 mt-14`, `min-h-screen`, `bg-white` - **Page content padding**: `p-6` on all sides ### Page Header Every page has a header section directly below the top nav, inside the main area: ```html

住宅出售

``` ### Sidebar Navigation - Active item: `bg-orange-50 text-orange-600 font-medium border-r-2 border-orange-500` - Inactive item: `text-gray-600 hover:bg-gray-100` - Section label: `text-xs font-semibold text-gray-400 uppercase tracking-wide px-3 mt-4 mb-1` - Item height: `h-9` (`36px`) - Item padding: `px-3` - Icon size: `w-4 h-4`, `mr-2` - Second-level items: `pl-9` --- ## Core Component Specs ### Button **Variants**: | Variant | Tailwind classes | Use case | Never use for | |---|---|---|---| | `primary` | `bg-orange-500 hover:bg-orange-600 text-white font-medium rounded` | Single main CTA per view | Destructive actions | | `secondary` | `bg-white border border-gray-300 hover:bg-gray-50 text-gray-700 font-medium rounded` | Secondary actions | Main CTA | | `ghost` | `bg-transparent hover:bg-gray-100 text-gray-600 font-medium rounded` | Toolbar actions, low-priority | Standalone CTAs | | `danger` | `bg-red-600 hover:bg-red-700 text-white font-medium rounded` | Irreversible destructive actions only | Anything reversible | | `link` | `text-orange-500 hover:text-orange-600 underline-offset-2 hover:underline` | Inline navigation links only | Form submissions | **Sizes**: | Size | Height | Padding | Font | |---|---|---|---| | `sm` | `h-7` (28px) | `px-3` | `text-xs` | | `md` | `h-9` (36px) | `px-4` | `text-sm` — default | | `lg` | `h-11` (44px) | `px-6` | `text-base` | **States** (all must be handled in every button): - `default` — base styles above - `hover` — defined in variant above - `active` — `active:scale-95 active:opacity-90` - `focus-visible` — `focus-visible:outline-2 focus-visible:outline-orange-500 focus-visible:outline-offset-2` - `disabled` — `disabled:opacity-50 disabled:cursor-not-allowed disabled:pointer-events-none` - `loading` — spinner icon replaces or precedes label; button is disabled **Loading state rule**: Show spinner and disable button immediately on click. Never allow double-submission. ```html ``` **Icon buttons**: Always include `aria-label`. Never use icon-only buttons as the sole primary CTA. --- ### Form Inputs **Anatomy** (always in this order, no exceptions): ``` [Label] ← always visible, never placeholder-only [Input field] [Helper text] ← optional, describes expected format [Error message] ← replaces helper text on error ``` **Base input class**: ``` block w-full rounded border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-orange-500 disabled:bg-gray-100 disabled:text-gray-400 disabled:cursor-not-allowed ``` **States**: | State | Additional classes | |---|---| | `default` | `border-gray-300` | | `focus` | `ring-2 ring-orange-500 border-orange-500` | | `error` | `border-red-500 ring-2 ring-red-500 focus:ring-red-500` | | `disabled` | `bg-gray-100 text-gray-400 cursor-not-allowed` | | `readonly` | `bg-gray-50 text-gray-600 cursor-default` | **Label**: ```html ``` **Helper text**: ```html

格式说明文字

``` **Error message**: ```html ``` **Rules**: - Label always above input, never to the side (exception: checkbox and radio) - Placeholder text is NOT a label — both must exist - Error messages: specific and actionable ("请输入有效的手机号", not "格式错误") - Required fields: mark with `*` next to label; explain at top of form ("* 为必填项") - Never disable a submit button to prevent submission — show errors inline instead - `aria-describedby` must link input to its error element when error is shown **Select / Dropdown**: ``` block w-full rounded border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-orange-500 ``` **Textarea**: same as input, add `resize-y min-h-[80px]`. Always include character counter when there is a max length. --- ### Data Table **Structure**: ``` [Toolbar: bulk actions + filter chips + column visibility + export] [Table: sticky header, checkbox col, data cols, actions col] [Pagination: count + page controls + per-page selector + jump-to] ``` **Table base classes**: ```html
列名 ...
... ...
``` Add `group` class to `` to enable `group-hover` on the actions column. **Column rules**: - Numbers: `text-right font-mono tabular-nums` - Dates: relative time for < 7 days ("2小时前"), absolute date for older ("2025-10-15") - Status: always a colored badge — never plain text - Long text: `truncate max-w-[200px]` with `title` attribute showing full value **Default page size**: 25 rows. Options: 10 / 25 / 50 / 100. **Pagination display**: Always show total count — "共 3,629 条,第 1–25 条" **Empty state** (never just "暂无数据"): ```html

暂无房源

符合条件的房源将出现在这里

新增房源
``` --- ### Status Badge Status must always be communicated with **color + icon + text** (never color alone). ```html 状态文字 ``` **Variant classes** (add to base): | Status | Class | |---|---| | Active / 在售 | `bg-green-100 text-green-700` | | Warning / 即将过期 | `bg-yellow-100 text-yellow-700` | | Danger / 已删除 | `bg-red-100 text-red-700` | | Neutral / 已下架 | `bg-gray-100 text-gray-600` | | Info / 跟进中 | `bg-blue-100 text-blue-700` | | Brand / 出售 | `bg-orange-100 text-orange-700` | --- ### Modal / Dialog **Size variants**: | Size | Max-width | Use case | |---|---|---| | `sm` | `max-w-sm` (400px) | Confirmation dialogs, simple alerts | | `md` | `max-w-lg` (560px) | Forms with ≤ 5 fields | | `lg` | `max-w-2xl` (720px) | Complex forms, detail previews | | `xl` | `max-w-4xl` (960px) | Multi-step flows, wide content | **Base modal structure**: ```html
``` **Rules**: - Always trap focus inside modal (`x-trap` from Alpine.js Focus plugin) - ESC key always closes (Alpine handles this automatically with `x-trap`) - Click outside backdrop closes — unless form has unsaved changes → show confirmation - Never nest modals — use a multi-step flow instead - Destructive confirm dialogs: danger button on RIGHT, cancel on LEFT - Never auto-close a modal after an async action — wait for user to dismiss --- ### Drawer / Slide-over Panel Used for editing content with many fields where the main page should remain visible for reference. **When to use Drawer vs Modal**: | Scenario | Component | |---|---| | Many fields, needs scrolling | Drawer | | Few fields (≤ 5), simple confirm | Modal | | User needs to reference main page data while editing | Drawer | **Standard drawer** (slides in from the right): ```html
``` **Width**: `w-[480px]` default; `w-[640px]` for wide content (image management, multi-column settings). --- ### Tab Navigation **Standard tabs** (underline style): ```html
...
``` **Tab + HTMX** (for server-rendered tab content): ```html
...
``` **URL-syncing tabs**: Use `hx-push-url="true"` on HTMX tab requests to make tabs bookmarkable. --- ### Toggle Switch ```html ``` --- ### Collapsible / Accordion ```html
分组标题
``` Requires Alpine.js `@alpinejs/collapse` plugin (official, ~1KB). --- ### Tree Select For hierarchical data selection (org unit, staff assignment): - **Small datasets** (< 200 nodes): Alpine.js renders full JSON tree client-side - **Large datasets**: HTMX lazy-loads child nodes on expand Alpine.js data structure: ```javascript { open: false, query: '', selected: null, nodes: [/* tree JSON from Django */], toggle(node) { node.expanded = !node.expanded }, select(node) { this.selected = node; this.open = false }, filteredNodes() { if (!this.query) return this.nodes // Recursive filter — preserves ancestors of matching nodes return this.filterTree(this.nodes, this.query.toLowerCase()) } } ``` --- ### Multi-select Tag Input For multi-value fields (e.g., property status, amenities): ```html
暂无匹配选项
``` --- ### Date Range Picker Use **Flatpickr** (CDN, ~16KB, zero framework dependency): ```javascript flatpickr("#date-range-input", { mode: "range", showMonths: 2, dateFormat: "Y-m-d", locale: "zh", }); ``` Override Flatpickr default styles with Tailwind to match our design system. Never build a date picker from scratch. --- ### Photo Gallery / Image Management - Upload: **Filepond** (~50KB, zero framework dependency) — drag-and-drop, preview, progress, multi-file queue - Drag-to-reorder: **SortableJS** (~3KB) — use `handle: '.drag-handle'` - Lightbox preview: **Viewer.js** (~5KB) — zoom, rotate, thumbnail strip All three are framework-free pure JS libraries, fully compatible with HTMX + Alpine.js. --- ## State & Feedback Patterns ### Loading States | Duration | Pattern | |---|---| | < 300ms | Nothing — avoid flash of spinner | | 300ms – 1s | Inline spinner (`animate-spin`) | | 1s – 3s | Spinner + "加载中..." text | | > 3s | Progress bar + estimated time | | Background Celery task | Subtle pulsing indicator in top nav | HTMX automatically adds `htmx-request` class during requests — use it to show/hide indicators: ```html
``` --- ### Empty States Every list, table, and data view must handle the empty state. Required elements: 1. Relevant icon (not a generic "no data" icon — use something contextually relevant) 2. Friendly headline ("暂无房源") 3. Explanation ("符合当前筛选条件的房源将出现在这里") 4. CTA if the user can fix it ("新增第一条房源 →") ```html

暂无房源

符合当前筛选条件的房源将出现在这里

新增房源
``` --- ### Toast Notifications | Type | When to use | Duration | Classes | |---|---|---|---| | `success` | Async action completed | 3s auto-dismiss | `bg-green-50 border-green-200 text-green-800` | | `error` | Action failed, user must retry | Persistent (manual dismiss) | `bg-red-50 border-red-200 text-red-800` | | `warning` | Completed with caveats | 5s | `bg-yellow-50 border-yellow-200 text-yellow-700` | | `info` | Background process started | 3s | `bg-blue-50 border-blue-200 text-blue-700` | **Rules**: - Max 3 toasts visible at once (queue the rest) - Never show success toast for page navigations - Error toasts must include a retry action button when possible - Never use toast for validation errors — show inline instead - Position: `fixed bottom-4 right-4 z-50 flex flex-col gap-2` Trigger via HTMX response header: `HX-Trigger: {"showToast": {"type": "success", "message": "保存成功"}}` --- ### Inline Edit (Read/Edit Mode Toggle) For settings pages and detail views that support in-place editing: ```html
工龄计算方式
``` **Cancel rule**: Always snapshot data before entering edit mode. On cancel, restore from snapshot (3 lines). Never leave the user with unsaved partial edits on cancel. --- ## Responsive Breakpoints **Strategy**: Desktop-first (target users are ≥ 85% desktop/Windows Electron client). | Breakpoint | Tailwind prefix | Viewport | Target | |---|---|---|---| | Desktop | (base, no prefix) | > 1280px | Primary design target | | Laptop | `lg:` | ≥ 1024px | Minor layout adjustments | | Tablet | `md:` | ≥ 768px | Collapsed sidebar | | Mobile | `sm:` | ≥ 640px | Single column, no tables | **Component-specific rules**: - Data tables → `overflow-x-auto` on `md` and below - Sidebar → collapses to icon-only or hidden on `md`; bottom nav on `sm` - Modals → full-screen (`inset-0 rounded-none`) on `sm` - Multi-column forms → single column on `md` and below (`md:grid-cols-1`) - Drawers → full-width on `sm` (`sm:w-full`) **Never**: - Hide critical functionality on mobile — adapt it, do not remove it - Use fixed px widths for layout containers (use `max-w-*` instead) - Assume touch input on desktop --- ## Motion & Animation **Principle**: Motion communicates state, it does not decorate. **Duration scale**: | Token | Duration | Use for | |---|---|---| | `duration-100` | 100ms | Micro-interactions (checkbox tick, toggle) | | `duration-200` | 200ms | Most transitions (hover, focus, fade) — default | | `duration-300` | 300ms | Larger elements (modal enter, drawer slide) | | `duration-500` | 500ms | Page-level transitions only | **Easing**: - Default UI transitions: `ease-out` (enter) / `ease-in` (leave) - Playful/spring: avoid in this product — we are a business tool - Progress bars: `linear` **What to animate**: - `opacity` — enter/exit fades - `transform: translateY / translateX` — panel slide-in - `max-height` with `x-collapse` — accordion expand **Never animate**: - `width` or `height` directly — use `max-height` with `x-collapse` or `transform` - `top / left / right / bottom` — use `transform: translate` instead - `box-shadow` on hover — use opacity-layered pseudo-element trick instead - Anything if `prefers-reduced-motion: reduce` is set: ```css @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } } ``` --- ## Accessibility (Non-negotiable) **Every component must**: - Meet WCAG 2.1 AA contrast ratios (4.5:1 for body text, 3:1 for large text ≥ 18px) - Be fully keyboard navigable (Tab, Shift+Tab, Enter, Space, Escape) - Have visible focus indicators — never `outline: none` without a custom replacement using `focus-visible:` - Work with screen readers (proper ARIA roles and labels) **Required patterns**: | Element | Requirement | |---|---| | Icon-only buttons | `aria-label="操作名称"` always | | Form inputs | `id` attribute + `