@dloizides/ui-nav

中文文档
1.38.1发布于 2026/11/11MITTypeScriptESM + CJS近 30 天下载 3561

Config-driven, brand-agnostic React Native (RN-web) navigation shell — Sidebar + Topbar promoted from the byte-identical erevna-web/katalogos-web nav twins. Renders a caller-supplied NavItem[] (pre-localized labels, icon render slots), routes colours thro

原始使用文档
文档目录14 节
  1. Install
  2. Usage
  3. Horizontal top bar ( NavBar )
  4. One component, two orientations ( Nav )
  5. One shell, three layouts — top | side | both ( NavShell )
  6. Brand position, rail width and phone drawer ( NavShell , NA…
  7. Three-tier responsive rail ( AppShell.collapsedSidebar / Co…
  8. Dark bar over a light page ( NavBar.barTheme )
  9. Full-bleed bar with a centred content column ( NavBar.conte…
  10. Cross-navigation pills ( PillNav )
  11. Colour-scheme control ( DarkModeControl )
  12. Search — desktop inline field · mobile ⌘K palette
  13. Role gating
  14. License

@dloizides/ui-nav

Config-driven, brand-agnostic React Native (RN-web) navigation shell for the dloizides.com portfolio: Sidebar + Topbar, promoted from the byte-identical erevna-web / katalogos-web nav twins. The chrome is shared; the item data stays with your app — the components render a caller-supplied NavItem[] (labels already localized, icons as render slots) and route every colour through the shared @dloizides/ui-feedback UiProvider theme.

Install

npm install @dloizides/ui-nav @dloizides/ui-feedback @dloizides/auth-web

Peer deps: @dloizides/ui-feedback >= 1.2.0, @dloizides/auth-web >= 1.5.0, react >= 18, react-native >= 0.74 (use react-native-web on web).

Usage

import { Sidebar, Topbar, accessibleNavItems, type NavItem } from '@dloizides/ui-nav';

const items: NavItem[] = buildItems(); // your role-filtered / grouped items

<Sidebar
  items={items}
  pathname={usePathname()}
  onNavigate={(route) => router.push(route)}
  title={FM('menu.title')}
  regionLabel={FM('accessibility.navigationRegion')}
  navigateHint={(label) => FM('menu.navigateToHint', label)}
  expandHint={FM('menu.expandSection')}
  collapseHint={FM('menu.collapseSection')}
  renderChevron={(open, color, size) => <SvgIcon name={open ? 'chevronUp' : 'chevronDown'} color={color} size={size} />}
  header={<HomeShortcut />}
  footer={<><DarkModeToggle /><LogoutButton /></>}
/>

<Topbar
  left={<TenantLogo />}
  language={{ label: FM(`topbar.lang.${locale}`), hint: FM('topbar.langHint'), onPress: toggleLocale }}
  notificationSlot={<NotificationBell />}
  user={{ name: displayName, email }}
  logout={{ label: FM('topbar.logout'), hint: FM('topbar.logoutHint'), onPress: logout, testID: 'logout-button' }}
/>

Horizontal top bar (NavBar)

Prefer the nav links on top (a horizontal menu bar) instead of the left rail? NavBar renders the SAME NavItem[] model horizontally — brand slot on the left, inline links, a free-form right slot (auth / user / logout) — and collapses the links behind a hamburger below a configurable breakpoint (default 760, matching the v1 AML console .global-nav). It is purely additive; Sidebar is unchanged.

import { NavBar, type NavItem } from '@dloizides/ui-nav';

<NavBar
  items={items}                              // same role-filtered NavItem[] as Sidebar
  pathname={usePathname()}
  onNavigate={(route) => router.push(route)}
  regionLabel={FM('accessibility.navigationRegion')}
  navigateHint={(label) => FM('menu.navigateToHint', label)}
  brand={<BrandLogo />}                       // left slot
  right={<AuthLink />}                         // right slot (auth / user / logout)
  menuLabel={FM('menu.toggle')}               // a11y label for the hamburger
  menuHint={FM('menu.toggleHint')}
  collapseBelow={760}                          // hamburger under this viewport width
  overflowLabel={FM('menu.more')}             // "More ▾" priority+ trigger label
  overflowHint={FM('menu.moreHint')}
/>

Priority+ overflow ("…" menu). Above collapseBelow the links sit on ONE non-wrapping row. When they don't all fit, a measure pass keeps the leading items that fit inline and collapses the rest behind a "More ▾" trigger — the shared @dloizides/ui-layout ModalDropdown — so no item is ever cut off; the surplus moves into a portalled, keyboard-navigable dropdown. If the active route lives in the overflow set, the trigger wears the accent pill so it stays reachable. Localize the trigger with the optional overflowLabel (visible + accessible, default "More") and overflowHint. Below collapseBelow the whole set still collapses into the responsive hamburger drawer, so the two mechanisms never overlap.

One component, two orientations (Nav)

Nav renders EITHER the horizontal NavBar or the vertical Sidebar from the same NavItem[], chosen by a single orientation prop — flip between a top bar and a side rail without swapping components or re-plumbing props. orientation defaults to "horizontal"; the remaining props are a discriminated union of the delegate's props (TypeScript enforces the right set per orientation — e.g. title only on vertical, collapseBelow / overflowLabel only on horizontal).

import { Nav, type NavItem } from '@dloizides/ui-nav';

<Nav orientation="horizontal" items={items} pathname={p} onNavigate={go} regionLabel={r} />  // NavBar
<Nav orientation="vertical"   items={items} pathname={p} onNavigate={go} title={t} regionLabel={r} />  // Sidebar

Nav is purely additive: Sidebar / Topbar / NavBar stay exported and unchanged.

One shell, three layouts — top | side | both (NavShell)

Where Nav answers the single-axis question (a NavBar or a Sidebar), NavShell owns the whole page composition and renders a top bar, a side rail, or BOTH together, chosen by a single layout prop. It wires a top bar into AppShell's header slot and a side rail into its sidebar slot, and inherits every AppShell capability (auth gate, loading/error/forbidden state cards, width policy, banner, the responsive hamburger + overlay drawer).

import { NavShell, type NavItem } from '@dloizides/ui-nav';

// layout="both" — a top bar (brand / global links + overflow "…" / user) AND a
// side rail (sections) together (the AML-console shape), now first-class.
<NavShell
  layout="both"
  pathname={usePathname()}
  onNavigate={(route) => router.push(route)}
  regionLabel={FM('a11y.nav')}
  topBar={{
    items: globalItems,                 // top links (overflow-capable)
    brand: <BrandLogo />,
    right: <><LanguageSwitcher /><UserChip /></>,
    overflowLabel: FM('menu.more'),
    barTheme: darkChromeTheme,          // optional: dark bar over a light page
    contentMaxWidth: 1180,              // optional: full-bleed bar, inner capped + centred
  }}
  sideRail={{
    items: sectionItems,                // side sections (leaf + expandable)
    title: FM('menu.title'),
    footer: <><DarkModeControl … /><LogoutButton /></>,
    collapsed: { range: { min: 768, max: 1024 } },   // optional 3-tier icon rail
  }}
  gate={{ pending, authenticated, onRedirect: () => router.push('/login') }}
  state={{ loading, error, forbidden }}
  width={{ max: 1180, wideMax: 1400, wideMinViewport: 1600 }}
  testID="app-shell"
>
  <PageContent />
</NavShell>
  • layout="top" → a top bar only (topBar); no rail.
  • layout="side" → a header + a persistent left rail of sections (sideRail); pass a structured <Topbar> as header for the language/notification/user slots.
  • layout="both" → both, together.

NavShell composes existing components, so it stays purely additive — AppShell, NavBar, and Sidebar are unchanged.

Brand position, rail width and phone drawer (NavShell, NAV-BRAND-1)

One brand prop holds every choice; every default (NAV_BRAND_DEFAULTS) keeps today's layout.

brand fieldValuesDefault
placement'sidebar' (rail top) · 'topbar' (top-left of the header, >= 768 px, same background and bottom border as the top bar)'sidebar'
railWidth'default' 220 · 'sm' 240 · 'md' 264 · 'lg' 288 px (SIDEBAR_WIDTH_PX)'default'
drawerHeader'none' · 'brand' (brand once in the open drawer's header, with a close button)'none'
drawerWidth'default' (280 px panel + scrim) · 'full' (whole screen, close button)'default'
<NavShell layout="side" brand={{ placement: 'topbar', railWidth: 'sm', drawerHeader: 'brand', drawerWidth: 'full' }}
  sideRail={{ items, title, brand: <AppBrand /> }} header={<Topbar ... />} {...rest} />

The brand node is the same sideRail.brand in every position; only where it renders changes.

Three-tier responsive rail (AppShell.collapsedSidebar / CollapsedRail)

Supply a collapsedSidebar (or NavShell's sideRail.collapsed) to opt the rail into the erevna/katalogos scheme: the full rail on desktop, an intermediate persistent collapsed (icon-only) rail on a tablet band [min, max) (default { min: 768, max: 1024 }), and the overlay drawer on a phone. Omit it and the layout stays the unchanged two-tier scheme (full rail ≥768px, drawer below). CollapsedRail renders the icon rail from the same NavItem[] (each item's renderIcon, or a first-letter glyph fallback), with header/footer slots. The pure resolveRailMode decides the tier and is directly unit-testable.

Dark bar over a light page (NavBar.barTheme)

Pass barTheme (a UiTheme) to re-provision the theme for the bar only — a dark appbar above a light workspace (the AML v1 console). The live t / navigate are re-passed so labels keep localizing; every colour still flows through the theme (style-only). Custom appbar backgrounds (e.g. a radial-glow gradient) stay expressible via containerStyle.

Full-bleed bar with a centred content column (NavBar.contentMaxWidth)

Pass contentMaxWidth (px) — via NavBar.contentMaxWidth or NavShell.topBar.contentMaxWidth — to make the bar full-bleed while its content stays in a centred column. The OUTER container (which carries the bar background + bottom border from barTheme / theme surface + any containerStyle, e.g. AML's radial-glow appbar) spans the full viewport width; the INNER row (brand + links + right) is capped to contentMaxWidth and centred — the v1-console .ui-appbar (full-bleed) + .ui-appbar-inner { max-width; margin: 0 auto } shape. Omit it and the inner spans the full container (unchanged). The priority+ overflow "…" fit measures the capped inner's rendered width, so it keeps working against the content column, not the full-bleed container.

Cross-navigation pills (PillNav)

A row of fully-rounded pills from a NavItem[] — kefi's coral dashboard switcher. The active pill wears the accent (theme palette.primary). Folds in kefi's rule: it hides (renders null) unless there are at least minItems entries (default 1; kefi passes 2). Drop it into AppShell/NavShell's nav slot.

Colour-scheme control (DarkModeControl)

A controlled Light / Dark / System control (erevna/katalogos), in two variants: "segmented" (a highlighted segment row) and "cycle" (an icon+label button that advances to the next option per press). Options are pre-localized and value-agnostic (strings), so map your own preference enum without this package importing it. Put it in a Sidebar/CollapsedRail footer or a NavBar right slot.

An app that renders a Sidebar today switches to a top bar by swapping the Sidebar element for NavBar (same items / pathname / onNavigate) — e.g. the AML v2 app drops NavBar into the AppShell header (or nav) slot so its links sit across the top like the rest of the console.

Each link is a rounded-pill target with the v1 .gn-links affordance: muted at rest, brightening its text + showing a subtle pill background on hover, and a solid accent pill when active. All colours are theme-driven (rest = textSecondary, hover text = text, hover bg = surfaceElevated, active/ring = palette.primary); hover/focus are web-only and the hover cross-fade honours prefers-reduced-motion.

Accessibility. The container is a role="navigation" landmark named by regionLabel; active links expose aria-current="page" + role="link"; the responsive hamburger carries role="button", aria-expanded, and aria-controls pointing at the collapsible links region; every link/toggle is keyboard-operable (Enter/Space) with a themed focus ring and a ≥44×44 touch target. Sidebar leaves carry aria-current="page" and Topbar buttons a focus ring under the same bar.

Search — desktop inline field · mobile ⌘K palette

The rail can be searched two ways, chosen by viewport. resolveSearchAffordance(viewport) is the pure rule: 'inline' at/above DEFAULT_SEARCH_BREAKPOINT (768px), 'palette' below.

  • Desktop (wide): opt the Sidebar into a persistent inline field that narrows the nav list live as you type. It is OFF by default — pass enableInlineSearch + pre-localized search labels:

    <Sidebar
      items={items} pathname={p} onNavigate={go} title={t} regionLabel={r}
      enableInlineSearch
      search={{
        placeholder: FM('nav.search'), hint: FM('nav.searchHint'),
        clearLabel: FM('nav.clear'), clearHint: FM('nav.clearHint'),
        emptyText: FM('nav.noMatches'),
      }}
    />
    

    The sidebar owns the query (uncontrolled) unless you pass searchQuery + onSearchChange. Filtering is the pure filterNavItems(items, query) (all query tokens must match a label; a matched group keeps its children, a group with a matched descendant is kept pruned to it).

  • Mobile (narrow): keep the ⌘K CommandPalette (a persistent field costs too much vertical space). The useCommandPaletteHotkey shortcut stays bound at every width.

So a shell renders resolveSearchAffordance(width) === 'inline' ? <Sidebar enableInlineSearch …/> : <CommandPalette …/> (or simply passes enableInlineSearch only on the full-rail branch).

Role gating

accessibleNavItems(user, roleRouteTable, translate) builds the NavItem[] a user's roles unlock, reusing resolveAccessibleRoutes from @dloizides/auth-web (most privileged first, empty when none) — no duplicated role logic.

License

MIT

版本与发布记录

@dloizides/ui-nav 共发布 48 个版本,首次发布于 2026/07/09;过去 12 个月发布了 48 个版本。

版本发布日期距今
1.38.1latest2026/11/111 天前
1.38.02026/11/111 天前
1.37.02026/11/111 天前
1.36.02026/11/102 天前
1.34.02026/11/102 天前
1.33.02026/11/102 天前

关于 @dloizides/ui-nav 的常见问题

@dloizides/ui-nav 是什么?

Config-driven, brand-agnostic React Native (RN-web) navigation shell — Sidebar + Topbar promoted from the byte-identical erevna-web/katalogos-web nav twins. Renders a caller-supplied NavItem[] (pre-localized labels, icon render slots), routes colours thro

如何安装 @dloizides/ui-nav?

在项目目录运行 npm install @dloizides/ui-nav,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:react(>=18.0.0)、react-native(>=0.74.0)、@dloizides/auth-web(>=1.5.0)、@dloizides/ui-layout(>=1.41.0)、@dloizides/ui-feedback(>=1.2.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@dloizides/ui-nav 支持 TypeScript 吗?

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

@dloizides/ui-nav 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@dloizides/ui-nav 需要什么版本的 Node.js?

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

@dloizides/ui-nav 可以商用吗?

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

@dloizides/ui-nav 的下载量有多少?

根据 npm 官方下载统计,@dloizides/ui-nav 在 2026/10/13 – 2026/11/11 这 30 天内被下载 3,561 次,日均约 119 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@dloizides/ui-nav 还在维护吗?

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