mcpField
D
desktop-principles

desktop-principles

github.com/athevon设计与 UI698981 次安装3 周前更新
代码仓库

关于 desktop-principles

面向 Web、macOS、Windows 和 Linux 桌面界面的 UX 技能,提供指针反馈、键盘操作、窗口、焦点、信息密度与克制动效的实现指引。

主要功能

  1. 01悬停与指针交互指引
  2. 02键盘快捷键与焦点
  3. 03多窗口模式
  4. 04桌面信息密度
  5. 05克制动效原则

适用场景

  1. 01为桌面控件添加清晰的悬停反馈
  2. 02规划快捷键和可见的键盘焦点
  3. 03判断使用新窗口还是临时面板

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/athevon/genjutsu --skill desktop-principles

安装后在 Claude Code 中输入/desktop-principles确认已加载。

详细介绍

版本敏感。 以下所有 API 名称、SDK 门槛和浏览器支持声明均已于 2026-09-08 根据一手资料核实。核实依据和时间见 _jutsu/VERSIONS.md。如果该日期已久远,请在使用版本号前重新核实。

桌面端原则

桌面端 UX 背景。检测到桌面环境(macOS、Windows、Linux 桌面端、Web 桌面端)时加载。 此处提供简明规则。深入内容见 references/。


悬停状态不可或缺

在桌面端,悬停是主要的交互提示,与移动端相反。指针悬停在目标上却没有即时视觉反馈,会让人觉得界面出了问题:用户依赖 :hover 在点击前确认元素是否可交互。每个可点击的界面区域都必须有明显的悬停样式,最好设置 100-200ms 的过渡效果,让变化清晰可感,又不会显得迟缓。

CSS - 交互元素的悬停样式:

.button {
  background: var(--surface);
  transition: background 120ms ease-out, transform 120ms ease-out;
}

.button:hover {
  background: var(--surface-hover);
  transform: translateY(-1px);
}

.button:active {
  transform: translateY(0);
}

SwiftUI - .onHover 是通用的悬停信号;.hoverEffect 仅适用于 iOS/iPadOS,必须通过 #if 进行条件编译:

修饰符支持情况说明
.onHover { Bool }iOS 13.4+、iPadOS 13.4+、Mac Catalyst 13.4+、macOS 10.15+、visionOS 1.0+在 macOS 上可用的修饰符
.onContinuousHover { HoverPhase }iOS 17.0+、macOS 14.0+、tvOS 17.0+、visionOS 1.0+提供指针位置,不只是进入/移出状态
.hoverEffect(_:)iOS 13.4+、iPadOS 13.4+、Mac Catalyst 13.4+、tvOS 16.0+、visionOS 1.0+ - macOS 不支持在 macOS 目标中会导致编译错误
.pointerStyle(_:)macOS 15.0+、visionOS 2.0+更改视图上的 macOS 光标
struct ToolbarButton: View {
  @State private var hovering = false

  var body: some View {
    Image(systemName: "square.and.arrow.up")
      .padding(8)
      .background(hovering ? Color.gray.opacity(0.15) : .clear)
      .onHover { hovering = $0 }
      .animation(.easeOut(duration: 0.12), value: hovering)
      .pointerHighlight() // see the extension below - .hoverEffect is UNAVAILABLE on macOS
  }
}

// `.hoverEffect` is @available(macOS, unavailable). Gate it with #if, never with #available.
extension View {
  @ViewBuilder func pointerHighlight() -> some View {
    #if os(macOS)
    self
    #else
    self.hoverEffect(.highlight)
    #endif
  }
}

Compose Desktop - 使用 onPointerEvent 或 hoverable + interactionSource:

@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun ToolbarButton(onClick: () -> Unit) {
  val interactionSource = remember { MutableInteractionSource() }
  val hovered by interactionSource.collectIsHoveredAsState()

  Box(
    modifier = Modifier
      .hoverable(interactionSource)
      .background(if (hovered) Color.LightGray.copy(alpha = 0.15f) else Color.Transparent)
      .clickable(onClick = onClick)
      .padding(8.dp),
  ) { Icon(Icons.Default.Share, contentDescription = "Share") }
}

指针精确度

鼠标和触控板指针远比拇指精准,因此桌面端目标可以小于移动端的 44pt 最小值。常见范围是图标按钮 24-32px、工具栏项 28-36px。WCAG 2.5.8 Target Size (Minimum)(目标尺寸(最小))AA 级将底线设为24x24 CSS 像素,适用于所有指针输入,并非仅限桌面端。小于 24px 的目标仍可通过间距例外满足要求:以每个尺寸不足的目标为圆心,绘制直径为 24px 的圆,且任意两个圆都不得相交。Apple 的 macOS HIG 在理念上更严格、数值上更宽松:默认控件尺寸为 28x28 pt,绝对最小尺寸为 20x20 pt。

**实际应用 Fitts 定律:**获取目标所需的时间会随着目标尺寸增大而缩短,随着距离增加而延长。屏幕边缘和角落是深度无限的目标——无论指针滑过多远,都会停在那里。将高频全局控件(关闭窗口、系统菜单、应用程序 Dock)放在角落和边缘。macOS 菜单栏和 Windows 任务栏就是典型应用:锚定在边缘,获取目标时不会越过目标。


键盘快捷键(一级功能)

桌面端用户期待快捷键符合原生惯例。在列表繁多的应用中缺少 ⌘+F 不是极简主义,而是缺陷。

操作macOSWindows / Linux
新建⌘+NCtrl+N
关闭窗口⌘+WCtrl+W
退出应用⌘+QAlt+F4
设置 / 偏好设置⌘+,Ctrl+,
查找⌘+FCtrl+F
切换(注释、侧边栏……)⌘+/Ctrl+/
保存⌘+SCtrl+S
命令面板⌘+K 或 ⌘+Shift+PCtrl+K 或 Ctrl+Shift+P

Web - 正确检测 Ctrl 与 Cmd:

// Prefer event.metaKey on macOS, event.ctrlKey elsewhere.
// navigator.platform is deprecated but still pragmatic; fall back to userAgent.
// navigator.platform is always a non-empty string where it exists, so `||` never falls through.
// Prefer UA-CH where available, then platform, then the UA string.
const isMac = /Mac|iPhone|iPad/.test(
  navigator.userAgentData?.platform ?? navigator.platform ?? navigator.userAgent ?? ""
);

window.addEventListener("keydown", (e) => {
  const cmdOrCtrl = isMac ? e.metaKey : e.ctrlKey;
  if (cmdOrCtrl && e.key.toLowerCase() === "k") {
    e.preventDefault();
    openCommandPalette();
  }
});

SwiftUI - .keyboardShortcut 绑定到菜单命令:

Button("New Document", action: newDoc)
  .keyboardShortcut("n", modifiers: .command)

Button("Find", action: focusSearch)
  .keyboardShortcut("f", modifiers: .command)

Compose Desktop - onKeyEvent + KeyShortcut:

@OptIn(ExperimentalComposeUiApi::class)
fun Modifier.commandShortcut(key: Key, onTrigger: () -> Unit) =
  onKeyEvent { event ->
    if (event.type == KeyEventType.KeyDown && event.isMetaPressed && event.key == key) {
      onTrigger(); true
    } else false
  }

// In MenuBar:
MenuBar {
  Menu("File") {
    Item("New", shortcut = KeyShortcut(Key.N, meta = true), onClick = ::newDoc)
    Item("Find", shortcut = KeyShortcut(Key.F, meta = true), onClick = ::focusSearch)
  }
}

多窗口模式

桌面用户会将窗口并排放置。以下情况适合打开新窗口:

  • 任务耗时较长,用户希望继续在主窗口中工作(渲染、导出、同步日志)。
  • 用户需要比较两个并行的上下文(两份文档、两个聊天、两个问题)。
  • 应用以文档为中心,且每份文档都是同级对象(Pages、Figma 文件、Xcode 项目)。

对于临时确认、简短的设置面板,或可以放在工作表或弹出框中的内容,打开新窗口并不合适。

SwiftUI - 用于文档样式窗口的 WindowGroup,用于单例的 Window:

@main
struct MyApp: App {
  var body: some Scene {
    WindowGroup("Document") { DocumentView() } // peer windows, one per doc

    Window("Inspector", id: "inspector") { InspectorView() }
      .windowResizability(.contentSize) // tracks intrinsic content size

    Settings { SettingsView() } // ⌘+, target on macOS
  }
}

Compose Desktop - Window 可组合项、应用程序作用域:

fun main() = application {
  val docs = remember { mutableStateListOf(Document()) }

  docs.forEach { doc ->
    Window(onCloseRequest = { docs.remove(doc) }, title = doc.title) {
      DocumentView(doc)
    }
  }

  if (showInspector) {
    Window(onCloseRequest = { showInspector = false }, title = "Inspector") {
      InspectorView()
    }
  }
}

**状态共享:**窗口是同一模型上的视图。将事实来源保存在单例或 DI 作用域对象中(通过环境注入的 SwiftUI @Observable、Compose koin 或等效的 viewModel)。切勿为每个窗口复制状态——协调彼此分歧的副本会引发大量 bug。


焦点管理

键盘导航是桌面端的一等输入方式。Tab 顺序必须合理,焦点环必须清晰可见,移除焦点环又不提供替代方案会导致无障碍体验退步。

SwiftUI - @FocusState 控制字段焦点:

struct LoginForm: View {
  enum Field { case email, password }

  @State private var email = ""
  @State private var password = ""
  @FocusState private var focused: Field?

  var body: some View {
    VStack {
      TextField("Email", text: $email)
        .focused($focused, equals: .email)
        .onSubmit { focused = .password }
      SecureField("Password", text: $password)
        .focused($focused, equals: .password)
        .onSubmit(submit)
    }
    .onAppear { focused = .email }
  }
}

Compose - FocusRequester + LocalFocusManager:

val emailFocus = remember { FocusRequester() }
val passwordFocus = remember { FocusRequester() }
val focusManager = LocalFocusManager.current

TextField(
  value = email, onValueChange = { email = it },
  modifier = Modifier.focusRequester(emailFocus),
  // Without keyboardOptions.imeAction the IME never emits Next and onNext never fires.
  keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
  keyboardActions = KeyboardActions(onNext = { passwordFocus.requestFocus() }),
)

TextField(
  value = password, onValueChange = { password = it },
  modifier = Modifier.focusRequester(passwordFocus).focusable(),
  keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus(); submit() }),
)

LaunchedEffect(Unit) { emailFocus.requestFocus() }

Web - tabindex + :focus-visible:

.button {
  /* Never `outline: none` without an alternative. */
}

.button:focus-visible {
  outline: 2px solid var(--ring);
  outline-offset: 2px;
}

<div role="button" tabindex="0" class="button">Custom button</div>

信息密度

桌面用户面对 13-32 英寸的屏幕,使用精准的指针和完整键盘。他们每个视口能解析、也希望解析的信息比移动端更多。使用 8px 基础网格(移动端为 4-8px)、持久侧边栏而非底部标签页、面向高级用户的命令面板(⌘K),以及适合数据需求的高密度数据表。Linear、Things 3 和 Notion 是值得借鉴的范例:信息丰富却不显拥挤,每个像素都物尽其用。


微妙动画原则

桌面应用会被连续注视数小时。初次体验令人愉悦的动画,重复上百次后可能变得难以忍受。优先使用简短且纯粹功能性的动效:透明度变化和低于 200ms 的轻微位移;常规交互中不要使用弹跳效果,悬停时也不要俏皮地过度移动。富有表现力的动效只用于一次性场景(引导流程、成功状态),不要用于日常 UI。

/* BAD - every hover bounces for 600ms, exhausting after the third use */
.card {
  transition: transform 600ms cubic-bezier(0.34, 1.56, 0.64, 1);
}
.card:hover { transform: scale(1.05); }
/* GOOD - 100ms opacity, almost subliminal, never tires */
.card {
  opacity: 0.92;
  transition: opacity 100ms ease-out;
}
.card:hover { opacity: 1; }

反模式(BAD / GOOD)

1. 在桌面端将导航隐藏在汉堡菜单后

<header>
  <button class="hamburger" aria-label="Menu">☰</button>
</header>
<nav class="drawer hidden">...</nav>

<aside class="sidebar">
  <nav>
    <a href="/inbox">Inbox</a>
    <a href="/projects">Projects</a>
    <a href="/archive">Archive</a>
  </nav>
  <button class="collapse-toggle" aria-label="Collapse sidebar">⇤</button>
</aside>
2. 主要操作没有键盘快捷键
// BAD - "New" is buried in a menu, no shortcut, every creation is 3 clicks
<Toolbar>
  <Menu>
    <MenuItem onClick={newDoc}>New document</MenuItem>
  </Menu>
</Toolbar>
// GOOD - ⌘N baseline, surfaced in the menu, mirrored on the toolbar tooltip
<Toolbar>
  <button onClick={newDoc} title="New document (⌘N)">
    <PlusIcon />
  </button>
</Toolbar>
// Bound globally:
useShortcut("mod+n", newDoc);
useShortcut("mod+s", save);
useShortcut("mod+f", focusSearch);
3. 移除焦点环却不提供替代方案
/* BAD - keyboard users now have no idea where focus is */
button:focus { outline: none; }
/* GOOD - :focus-visible keeps mouse clicks ring-free, keyboard navigation visible */
button:focus { outline: none; }
button:focus-visible {
  outline: 2px solid var(--brand-500);
  outline-offset: 2px;
  border-radius: 6px;
}

快速参考:加载子技能

需求加载
深入了解键盘模式references/keyboard-patterns.md
多窗口状态references/multi-window.md
SwiftUI 动画../swiftui-motion/SKILL.md
Compose Desktop../compose-multiplatform/SKILL.md(当桌面端是 CMP 目标时)

来源

评论

支持 Markdown