.agents/skills/ux/SKILL.md
How LobeHub products should feel, and concrete rules to get there. Use this when building or reviewing any user-facing flow. For component/styling choices see react, for wording see microcopy, for imperative modal wiring see modal.
LobeHub follows four product design values — Natural・Meaningful・Certainty・ Growth. Read them before designing: references/design-values.md (definitions + conflict priority).
The checklists below are the execution layer. Each item is tagged with the value(s) it serves; for what those values mean, see the file above.
Use these principles before the execution checklists when a flow has multiple plausible interaction patterns.
Every surface carries a task promise: chat keeps the user in a working conversation, a document page supports focused reading / editing, a settings page supports configuration, and so on. Default interactions should continue that promise instead of unexpectedly moving the user into another mode. Prefer in-context surfaces (portal / panel / drawer) for reference and auxiliary work; reserve full-page navigation for committed focus or explicit mode switches.
Consistency means the same user intent behaves the same way in the same surface. It does not mean the same component must do the same thing everywhere. When a component is reused across surfaces, let the parent surface provide the interaction strategy so behavior follows intent rather than implementation convenience.
Element placement is part of the interface language. Identity and location (breadcrumbs, titles, object labels) should read separately from state and actions (save status, sharing, panel toggles, overflow menus). When these roles are mixed, users have to infer whether an element describes the current object or acts on it.
The checklists are grouped by interaction type — the kind of thing the user is doing. Jump to the module that matches the surface you're building (reading a list, editing content, running an action, …); each module collects the rules specific to that interaction. The same surface often spans several modules (an editable list is Read + Edit + Act) — walk each that applies.
Any surface that displays records, lists, or detail. Covers the states a data view can be in, behavior at scale, and keeping the user's place visible.
Every data surface has four states — design all of them, not just "has data".
+
affordance stays reachable), the body below it must still render an empty
placeholder — persistent chrome is not an excuse to leave the content area
blank. ✅ The agent Documents tab keeps its new-folder / new-doc toolbar
and renders an Empty below it when there are no documents — ❌ not a toolbar
over dead space. (Meaningful)A list/data page must be designed for its whole range of sizes, not just the demo data.
A capped / scrollable / virtualized list mounts at scrollTop = 0. If the
active item sits below the fold, the user lands on a valid selection that is
off-screen — and reads it as "nothing is selected" or a broken page. Any
list that can open with a pre-selected item must scroll that item into view.
This is an easy case to miss: it only shows up once the list is long enough and
the selection is restored rather than freshly clicked.
?thread= below the fold is scrolled into view on mount. (Certainty)block: 'nearest' (or equivalent). Only scroll when the row is
actually off-screen; an already-visible selection must not jump. (Natural)virtual flags, scope filters) and add them back.
✅ The default "LobeAI" (inbox) agent is virtual and excluded from the
sidebar list, so the move picker re-adds it. An empty picker must mean
"genuinely none", never "we filtered out the only option". (Meaningful)A surface with multiple tabs / views / panels has a landing selection. Don't hardcode it to "the first tab" — derive it from (a) how the user got here (the intent their navigation carried) and (b) which views actually have data. A static default that lands the user on an empty tab while a sibling holds exactly what they came for reads as broken. This pairs with §1.1: the empty state is the fallback within a view; this rule is about not landing on that empty view in the first place when a better one exists.
pickedTab
that overrides the derived default) so later data changes don't yank them off
their choice. (Natural)Any surface where the user types or edits. Input is expensive effort; the overriding rule is never lose it.
Typed / edited content is real user effort; losing it is one of the most infuriating outcomes a product can produce. Whenever an editor holds unsaved input, assume the exit can be accidental — a misclick, a refresh, a crash, a navigation, a failed save — and build a safety net: back the draft up locally and recover it.
Any surface where the user performs an action — a single op, a bulk op, or a multi-step flow. Covers momentum, focus, and full entity lifecycle.
Every action chain must push the user forward, never dead-end or block the flow.
The recurring trap: a feature ships only the display of a list, but edit / delete / management are never built — so the user can add something and then be stuck with it. For every entity a user can see, design its full lifecycle: create / read / update / delete, plus state transitions (enable/disable, connect/disconnect, install/uninstall). A read-only list the user can't manage breaks the flow.
The allowed operation set depends on the entity's source / ownership — decide it explicitly before building. Worked example, the tools/connectors list:
| Entity class | Add | Edit | Remove |
|---|---|---|---|
| Official / built-in (skills, tools) | — | — | ✗ not removable |
| Community (installed MCP) | install | configure | uninstall / remove |
| User-custom (custom connector) | create | edit | delete |
How the product answers back while and after the user acts — loading visuals and proactive guardrails.
Never use antd Spin — it doesn't match the product's loading visual. Use a
project loader:
| Need | Component |
|---|---|
| Default loading (in-flight) | NeuralNetworkLoading from @/components/NeuralNetworkLoading (size prop) |
| Inline dots | DotsLoading / BubblesLoading from @/components |
| Branded full-page | Loading from @/components/Loading/BrandTextLoading |
| List / card placeholder | a skeleton (e.g. SkeletonList) |
When in doubt, reach for NeuralNetworkLoading — it's the default in-flight
indicator (e.g. modal "in progress" states).
Minimise layout shift (CLS): swap text for skeleton in place, keep the chrome. The strongest loading state changes as little of the final layout as possible, so nothing jumps when the real content lands. When a surface already knows its shape (a card, a row, a list item), keep the layout elements — the container, border, radius, padding, icon — and replace only the text/data with a skeleton sized like the text it stands in for. A generic full-block / full-card skeleton (or a centred spinner that the real content later pushes aside) is heavier and shifts the layout; an in-place text→skeleton swap is the optimal design.
A feature can be fully built and still produce a broken result when the selected model — or its still-loading config — can't deliver the capability the feature depends on (for example, an agentic run on a model without tool calling). This is usually the user's configuration choice, not a defect; but if the product stays silent the user reads it as the product being broken. When a feature's success depends on a capability the current config may lack, the product owes a proactive, non-blocking reminder — a guardrail, not a gate.
How the product deepens as the user's needs deepen.
The product should grow with the user — deeper power shows up as needs deepen.
Read — viewing data & lists
block: 'nearest', re-run after async rows mount).Edit — entering & changing content
Act — operations, flows & buttons
Feedback — loading & system response
Spin; use NeuralNetworkLoading / project loaders.Grow — discoverability & progressive disclosure
createModal state-machine wiring for confirm/progress/done.Button usage, styling.