@iyulab/desktop-patterns

中文文档
0.13.0发布于 2026/11/01MITTypeScriptES Module构建来源已验证

App-shell and structural layout patterns assembled on top of desktop-compact's primitives. Framework-neutral custom elements (Lit), zero built-in strings, every content region a slot.

原始使用文档
文档目录10 节
  1. Install
  2. Usage
  3. A list beside the item picked from it
  4. Sidebar items: links, groups, icons, actions
  5. Theming
  6. Platform boundary — drag regions
  7. Components
  8. Accessibility
  9. Development
  10. License

desktop-patterns

App-shell and structural layout patterns assembled on top of desktop-compact's primitives. Framework-neutral custom elements (Lit), zero built-in strings, every content region a slot.

Structure only — atomic controls (buttons, inputs, dialogs) belong in desktop-compact; native platform integration (file dialogs, tray, window control) belongs to the desktop runtime's own adapter layer, whichever runtime the app uses. This package doesn't call any platform API itself, and knows nothing about any specific consuming application.

Install

Requires Node ≥22.

npm install @iyulab/desktop-patterns @iyulab/desktop-compact

Import only the components you use — each ships as its own subpath for tree-shaking:

import '@iyulab/desktop-patterns/shell'
import '@iyulab/desktop-patterns/sidebar'

Or import everything via the barrel:

import '@iyulab/desktop-patterns'

Usage

<dp-shell>
  <dp-sidebar slot="sidebar" active-id="overview" header="My App" id="nav"></dp-sidebar>
  <dp-toolbar slot="toolbar" heading="My App" show-toggle></dp-toolbar>
  <dp-page>
    <h2>Overview</h2>
    <p>Main content.</p>
  </dp-page>
</dp-shell>

items/bottomItems are arrays, so they're set as JS properties (not plain HTML attributes) — same as any Lit custom element:

document.querySelector<HTMLElementTagNameMap['dp-sidebar']>('#nav')!.items = [
  { id: 'overview', icon: '■', label: 'Overview' },
]

import { desktopMinWidth } from '@iyulab/desktop-patterns/breakpoints' // no elements registered

const shell = document.querySelector('dp-shell')!
const sidebar = document.querySelector('dp-sidebar')!
const toolbar = document.querySelector('dp-toolbar')!
const wide = matchMedia(`(min-width: ${desktopMinWidth}px)`)
let collapsed = false // the consumer's own state: a wide window's sidebar folded to its rail

function sync() {
  sidebar.collapsed = collapsed && wide.matches // a drawer always shows the whole sidebar
  toolbar.expanded = wide.matches ? !collapsed : shell.sidebarOpen
}
wide.addEventListener('change', sync)
sidebar.addEventListener('dp-sidebar-select', () => {
  // consumer owns activeId / routing
})
sidebar.addEventListener('dp-sidebar-activate', () => {
  shell.sidebarOpen = false // a drawer gives way to what was picked — the place already shown included
  sync()
})
toolbar.addEventListener('dp-toolbar-toggle', () => {
  if (wide.matches) collapsed = !collapsed
  else shell.sidebarOpen = !shell.sidebarOpen
  sync()
})
shell.addEventListener('dp-shell-sidebar-close', () => { shell.sidebarOpen = false; sync() }) // backdrop, Escape

Below the desktop breakpoint (desktopMinWidth, 1024px — desktopMedia for a component's styles) the sidebar is a drawer over the content, open while sidebar-open is set; at and above it the sidebar sits beside the content and sidebar-open changes nothing. The same toolbar toggle serves both widths: in a narrow window it opens the drawer, in a wide one it folds the sidebar to its collapsed rail (dp-sidebar collapsed). Give the toggle expanded so assistive technology hears which state it is in. The consumer owns both states — start the drawer closed, close it when the shell asks (backdrop click, Escape) and when a place is picked, or a narrow window stays covered.

The shell holds the window's height, so the document never scrolls — the main content box does. Anything that saves and restores where the user was reading reaches that box through shell.mainElement; with @iyulab/router, hand it to the router's scroll restoration: scrollRestoration: { container: () => document.querySelector('dp-shell')?.mainElement }.

A list beside the item picked from it

<dp-page fill max-width="full">
  <dp-list-detail>
    <nav slot="list" aria-label="People">…</nav>
    <article>…the person picked…</article>
  </dp-list-detail>
</dp-page>

dp-list-detail puts the list slot beside the default slot, each scrolling on its own, the list --dp-list-detail-list-width wide (by default the sidebar's width — navigation, list and item read as 1:1:rest). Below the desktop breakpoint there is room for one pane: the item while detail-open is set, the list otherwise. The consumer owns detail-open — set it when an item is picked, and give the item a way back that clears it, shown only below desktopMinWidth.

nav.items = [
  { id: 'overview', icon: '■', label: 'Overview', href: '/overview' }, // href → rendered as <a>
  {
    id: 'work', label: 'Work', collapsed: false,                        // a group: a disclosure over its items
    items: [{ id: 'runs', icon: '▶', label: 'Runs', href: '/runs' }],
  },
]
nav.pinnedItems = [{ id: 'settings', icon: '⚙', label: 'Settings' }]   // places kept at the foot of the rail
nav.bottomItems = [
  { id: 'dense', icon: '▤', label: 'Dense mode', pressed: false },    // a toggle: aria-pressed, owned by you
  { id: 'sign-out', icon: '⎋', label: 'Sign out' },                    // actions, not places
]
nav.setAttribute('nav-label', 'Main')                                 // names the navigation landmark
  • href renders the item as a link, so a router's link interception (or the browser) does the navigating. dp-sidebar-select still fires and is cancelable — preventDefault() cancels the navigation and keeps the active item. A modified click (Ctrl/⌘/Shift/Alt, middle button) opens the link elsewhere and selects nothing. Items without href stay buttons and the consumer navigates on dp-sidebar-select.
  • Groups ({ id, label, items, icon?, collapsed? }) render as a named role="group" under a disclosure button (aria-expanded). A group holding the active item is always open. In the collapsed rail every group's items show, separated by a rule.
  • Icons: each item's icon is a slot named icon-<id> whose fallback is the item's icon text — slot any icon element (<svg slot="icon-overview">) to replace it.
  • dp-sidebar-activate fires on every pick of a navigation item, the active one included (after dp-sidebar-select when the place changes; not when that was cancelled). Close a drawer on it: picking the place already shown still means "take me there", and dp-sidebar-select does not fire for it.
  • Pinned places (pinnedItems) sit at the foot of the rail, inside the same navigation landmark and outside the scrolling list. They are places like items — dp-sidebar-select, aria-current, href all apply — so a settings or help page shown there is marked as the current page.
  • Bottom items are actions: each click fires dp-sidebar-action (itemId) — every time, so a failed action can be retried — and never moves aria-current. (In 0.2.x they fired dp-sidebar-select and took the active mark.)
  • Toggle actions: a bottom action with pressed ({ id, icon, label, pressed: true | false }) is a setting it turns on and off. The button carries aria-pressed, and an on toggle gets a quiet ground and an accent icon (--dp-sidebar-pressed-bg, --dp-sidebar-pressed-icon-color) that still shows in the collapsed rail. Name it after the setting ("Dense mode"), not the click ("Turn dense mode on"). The sidebar does not flip it — change the value on dp-sidebar-action and set bottomItems again.

Every component takes its user-facing text as a plain attribute/property or slot — there is no built-in i18n layer. See each component's Storybook story (npm run storybook) for its full API.

Theming

Import both token sheets once in your app's global stylesheet:

import '@iyulab/desktop-compact/tokens.css'
import '@iyulab/desktop-patterns/tokens.css'

Color/spacing/radius/typography all come from desktop-compact's --dc-* tokens — this package adds only the structural dimensions desktop-compact has no concept of (--dp-sidebar-width, --dp-sidebar-width-collapsed, --dp-header-height, --dp-list-detail-list-width). Every component ships with sane fallback values, so it still renders correctly even without either stylesheet.

Platform boundary — drag regions

dp-toolbar exposes a neutral drag-region flag (not the native draggable, which already means something else) but never emits any platform-specific CSS itself. The flag reflects to the host attribute, but the actual drag surface is a dedicated part="drag-handle" spacer between the start group and the actions slot — never the whole host — so the mapping can never reach the toggle button or slotted actions. Mapping that part to an actual window-drag behavior is the job of the platform adapter layer, and how depends on the runtime. Where the webview honours the CSS app-region property (Chromium-based runtimes such as Electron), the mapping is one rule:

dp-toolbar[drag-region]::part(drag-handle) {
  -webkit-app-region: drag;
}

A runtime that starts a window drag from a pointer event or a marker attribute instead does that on the same part — never on the host.

dp-shell does not mark any drag region of its own — it has no opinion on what's slotted into toolbar, so ownership of the drag handle stays entirely with whatever renders there (typically dp-toolbar).

Components

ComponentDescription
dp-pageScrollable content region with a centered, max-width column — the page-body every view renders into. With fill it does not scroll and its column is as tall as the region, for a view that scrolls its own parts (a list beside the item open)
dp-list-detailA list beside the item picked from it (master-detail) — each pane scrolls on its own; one pane at a time below 1024px, the item while detail-open is set
dp-sidebarApp-shell navigation rail — expanded/collapsed states, an optional pinned bottom group, a footer slot for whatever a consumer wants to anchor there
dp-toolbarApp-shell header bar — heading/subtitle, an optional sidebar toggle (drawer in a narrow window, collapse in a wide one), a right-side actions slot
dp-shellTop-level layout composing sidebar/toolbar/banner/main-content regions, with a responsive drawer (backdrop + overlay sidebar) below a 1024px breakpoint
dp-shortcut-overlayDisplay-only keyboard-shortcuts help panel — a shortcuts list + open flag; no registration/binding infrastructure (that stays the consumer's)

Run npm run storybook to browse every component interactively, including dp-shell composing real dp-sidebar+dp-toolbar+dp-page together.

Accessibility

Every component ships with an axe accessibility test as part of its test suite (npm test). Landmarks follow ARIA APG conventions — dp-sidebar uses a semantic <nav> (no redundant host-level role), dp-shortcut-overlay uses role="dialog" with aria-labelledby pointing at its visible heading.

Development

npm install
npm test              # @web/test-runner, real Chromium
npm run typecheck
npm run guard          # platform-neutrality scan (src/ must not call or name a platform API)
npm run build          # per-component ESM output, type declarations
npm run storybook      # interactive component browser

tsc is TypeScript 7 (@typescript/native), while typescript resolves to @typescript/typescript6: TypeScript 7 has no JavaScript compiler API yet, and the declaration build reads one. The alias goes once vite-plugin-dts runs on TypeScript 7 alone.

@iyulab/desktop-compact is a peer dependency: this package reads its --dc-* tokens but never imports its code, so the consumer installs one copy and both packages share it. A regular dependency would give a consumer on a newer desktop-compact minor a second, unused copy nested under this package.

License

MIT

版本与发布记录

@iyulab/desktop-patterns 共发布 29 个版本,首次发布于 2026/09/03;过去 12 个月发布了 29 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.13.0latest2026/11/012 天前
0.12.22026/10/2410 天前
0.12.12026/10/2311 天前
0.12.02026/10/2311 天前
0.11.22026/10/1420 天前
0.11.12026/10/1321 天前

关于 @iyulab/desktop-patterns 的常见问题

@iyulab/desktop-patterns 是什么?

应用程序外壳和结构布局模式组装在上面 desktop-compact原语。框架无关的自定义元素(点燃),零内置字符串,每个内容区域都有一个槽。

如何安装 @iyulab/desktop-patterns?

在项目目录运行 npm install @iyulab/desktop-patterns,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:@iyulab/desktop-compact(^0.15.1 || ^0.16.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@iyulab/desktop-patterns 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@iyulab/desktop-patterns 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@iyulab/desktop-patterns 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=22。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@iyulab/desktop-patterns 可以商用吗?

@iyulab/desktop-patterns 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@iyulab/desktop-patterns 还在维护吗?

最新版本 0.13.0 发布于 2026/11/01(2 天前);至今共发布 29 个版本,其中过去 12 个月发布了 29 个;首次发布于 2026/09/03。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。