@dloizides/ui-nav 1.15.0 → 1.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,53 @@
1
1
  # Changelog
2
2
 
3
- ## 1.14.0
3
+ ## 1.16.1
4
+
5
+ - **Fix (NavShell forwards inline-search props to the inner `Sidebar`).** 1.16.0 added
6
+ desktop inline search to `Sidebar` (`enableInlineSearch`, `search`, controlled
7
+ `searchQuery` / `onSearchChange`), but all seven portals consume the higher-level
8
+ `NavShell` (via `sideRail`), which did **not** forward those props — so inline search was
9
+ unreachable by every real consumer. `NavShellSideRail` now carries the same four fields
10
+ (`enableInlineSearch?`, `search?: SidebarSearchConfig`, `searchQuery?`, `onSearchChange?`)
11
+ and threads them onto the `<Sidebar>` it builds. Desktop-vs-mobile resolution is unchanged
12
+ (inline ≥768, ⌘K palette <768 via `resolveSearchAffordance`).
13
+ - **Purely additive / backward compatible**: with the fields unset, `NavShell` renders
14
+ exactly as 1.16.0. No breaking change; no styling touched.
15
+
16
+ ## 1.16.0
17
+
18
+ - **Feature (docs-style sidebar rail — hover wash, tinted active pill, rotating chevron).**
19
+ The `Sidebar` / `NavExpandableItem` rows now read like the aml-screening docs navigation:
20
+ each row carries a PRIMARY-TINTED overlay whose opacity eases `0 → hover → active` as a
21
+ smooth ~160ms CSS cross-fade on web (gated by `prefers-reduced-motion`, omitted on native).
22
+ - **Hover** lights a row with a subtle brand wash (`HOVER_TINT_OPACITY` = 0.06); **active**
23
+ deepens it to a soft brand pill (`ACTIVE_TINT_OPACITY` = 0.14) over the transparent
24
+ left-accent gutter every leaf already reserved, so the active leaf shows a tinted pill + a
25
+ coloured left bar + brand-coloured label — no layout shift, all from `theme.palette.primary`.
26
+ - **Rotating chevron.** Expandable groups now get a built-in `SidebarChevron` (a `›` glyph
27
+ that rotates to `⌄` on expand, tweened on web) when the caller supplies no `renderChevron`.
28
+ A caller-supplied `renderChevron` still wins, so existing icon-set chevrons are unaffected.
29
+ - Row corner radius softened to `NAV_ROW_RADIUS` (8px) with the rounded overlay clipped to it.
30
+ - No public API change to existing props; hover uses RNW's `onHoverIn`/`onHoverOut` (no-op on
31
+ native). New exports: `SidebarChevron`, `NAV_ROW_RADIUS`, `ACTIVE_TINT_OPACITY`,
32
+ `HOVER_TINT_OPACITY`.
33
+
34
+ - **Feature (desktop INLINE sidebar search — opt-in — with the ⌘K palette kept for mobile).**
35
+ The owner found the ⌘K command-palette popup inconvenient on desktop. `Sidebar` now accepts
36
+ an opt-in persistent inline search field at the top of the rail that narrows the nav list
37
+ LIVE as you type.
38
+ - **New `Sidebar` props (all additive, default OFF):** `enableInlineSearch` + a pre-localized
39
+ `search: SidebarSearchConfig` (`placeholder` / `hint` / `clearLabel` / `clearHint` /
40
+ `emptyText`), plus optional controlled `searchQuery` + `onSearchChange` (the sidebar owns the
41
+ state when omitted). With the flag off the render tree is byte-identical to 1.15 — the mobile
42
+ drawer, which should keep using the ⌘K palette, is untouched.
43
+ - **New exports:** `SidebarSearch` (the field), `filterNavItems` / `isFilterActive` /
44
+ `searchTokens` (the pure tree-prune, same all-tokens-must-match semantics as `filterCommands`
45
+ so inline + ⌘K rank identically), and `resolveSearchAffordance(viewport)` +
46
+ `DEFAULT_SEARCH_BREAKPOINT` — the pure `'inline'` (≥768px) vs `'palette'` (<768px) rule so a
47
+ shell can wire desktop-inline / mobile-palette from one function. The ⌘K hotkey
48
+ (`useCommandPaletteHotkey`) stays bound at every width.
49
+
50
+ ## 1.15.0
4
51
 
5
52
  - **Feature (an expandable nav group now ANIMATES its expand/collapse instead of snapping).**
6
53
  `NavExpandableItem` used to conditionally render its children (`{expanded ? … : null}`),
package/README.md CHANGED
@@ -217,6 +217,37 @@ pointing at the collapsible links region; every link/toggle is keyboard-operable
217
217
  (Enter/Space) with a themed focus ring and a ≥44×44 touch target. `Sidebar` leaves
218
218
  carry `aria-current="page"` and `Topbar` buttons a focus ring under the same bar.
219
219
 
220
+ ### Search — desktop inline field · mobile ⌘K palette
221
+
222
+ The rail can be searched two ways, chosen by viewport. `resolveSearchAffordance(viewport)`
223
+ is the pure rule: `'inline'` at/above `DEFAULT_SEARCH_BREAKPOINT` (768px), `'palette'` below.
224
+
225
+ - **Desktop (wide):** opt the `Sidebar` into a persistent inline field that narrows the nav
226
+ list live as you type. It is OFF by default — pass `enableInlineSearch` + pre-localized
227
+ `search` labels:
228
+
229
+ ```tsx
230
+ <Sidebar
231
+ items={items} pathname={p} onNavigate={go} title={t} regionLabel={r}
232
+ enableInlineSearch
233
+ search={{
234
+ placeholder: FM('nav.search'), hint: FM('nav.searchHint'),
235
+ clearLabel: FM('nav.clear'), clearHint: FM('nav.clearHint'),
236
+ emptyText: FM('nav.noMatches'),
237
+ }}
238
+ />
239
+ ```
240
+
241
+ The sidebar owns the query (uncontrolled) unless you pass `searchQuery` + `onSearchChange`.
242
+ Filtering is the pure `filterNavItems(items, query)` (all query tokens must match a label;
243
+ a matched group keeps its children, a group with a matched descendant is kept pruned to it).
244
+
245
+ - **Mobile (narrow):** keep the ⌘K `CommandPalette` (a persistent field costs too much
246
+ vertical space). The `useCommandPaletteHotkey` shortcut stays bound at every width.
247
+
248
+ So a shell renders `resolveSearchAffordance(width) === 'inline' ? <Sidebar enableInlineSearch …/>
249
+ : <CommandPalette …/>` (or simply passes `enableInlineSearch` only on the full-rail branch).
250
+
220
251
  ### Role gating
221
252
 
222
253
  `accessibleNavItems(user, roleRouteTable, translate)` builds the `NavItem[]` a user's roles
package/dist/index.d.mts CHANGED
@@ -12,6 +12,45 @@ import { resolveAccessibleRoutes, RoleRouteTable, RoleRoute } from '@dloizides/a
12
12
  */
13
13
  declare function isRouteActive(pathname: string, route: string): boolean;
14
14
 
15
+ /**
16
+ * SidebarSearch — the persistent, DESKTOP inline search field that lives at the
17
+ * top of the {@link Sidebar} and narrows the nav list live as the operator types.
18
+ * On a wide viewport a field is cheap and one keystroke filters the rail; on a
19
+ * narrow viewport the ⌘K {@link CommandPalette} is used instead (a persistent
20
+ * field would steal scarce vertical space) — see {@link resolveSearchAffordance}.
21
+ *
22
+ * Package discipline (as the rest of ui-nav): no FM / router / icon-set imports.
23
+ * The placeholder + a11y strings are pre-localized by the caller; every colour is
24
+ * read from the `@dloizides/ui-feedback` UiProvider theme. A clear ("✕") button
25
+ * appears once there is a query, wired to `onClear`.
26
+ */
27
+
28
+ /** Pre-localized labels/strings for the inline search field. */
29
+ interface SidebarSearchLabels {
30
+ /** Placeholder + accessible name of the input, e.g. "Search". */
31
+ placeholder: string;
32
+ /** a11y hint describing what typing does. */
33
+ hint: string;
34
+ /** Accessible name of the clear ("✕") button. */
35
+ clearLabel: string;
36
+ /** Accessible hint of the clear ("✕") button. */
37
+ clearHint: string;
38
+ }
39
+ interface SidebarSearchProps {
40
+ /** Current query value (controlled). */
41
+ value: string;
42
+ /** Called with the new query on every keystroke. */
43
+ onChangeText: (query: string) => void;
44
+ /** Clear the query (also called by the ✕ button). */
45
+ onClear: () => void;
46
+ /** Pre-localized chrome strings. */
47
+ labels: SidebarSearchLabels;
48
+ /** testID for the input (the clear button is `${testID}-clear`). */
49
+ testID: string;
50
+ }
51
+ /** The inline sidebar search field (desktop) with a clear button. */
52
+ declare const SidebarSearch: ({ value, onChangeText, onClear, labels, testID, }: SidebarSearchProps) => React.ReactElement;
53
+
15
54
  /**
16
55
  * Public prop types for the `@dloizides/ui-nav` config-driven navigation shell.
17
56
  *
@@ -48,8 +87,19 @@ interface NavItem {
48
87
  * supplied `NavItem[]` (leaf + expandable), highlights the active route, and
49
88
  * exposes header/footer slots for app-specific chrome (title, dark-mode toggle,
50
89
  * logout, notification bell). Every colour is read from the UiProvider theme.
90
+ *
91
+ * Desktop inline search (v1.16, opt-in): pass `enableInlineSearch` + `search`
92
+ * labels and a persistent {@link SidebarSearch} field renders under the title and
93
+ * narrows the nav list live (via {@link filterNavItems}). It is OFF by default,
94
+ * so existing consumers — and the mobile drawer, which should use the ⌘K palette
95
+ * instead (see {@link resolveSearchAffordance}) — are byte-identical to before.
51
96
  */
52
97
 
98
+ /** Inline-search chrome strings — the field labels plus the no-match empty text. */
99
+ interface SidebarSearchConfig extends SidebarSearchLabels {
100
+ /** Shown in place of the list when the query matches nothing. */
101
+ emptyText: string;
102
+ }
53
103
  interface SidebarProps {
54
104
  /** Nav entries — already role-filtered / grouped by the app. */
55
105
  items: NavItem[];
@@ -69,6 +119,18 @@ interface SidebarProps {
69
119
  collapseHint?: string;
70
120
  /** Optional chevron renderer for expandable sections. */
71
121
  renderChevron?: (expanded: boolean, color: string, size: number) => React.ReactNode;
122
+ /**
123
+ * Opt into the persistent inline search field (desktop). Requires `search`
124
+ * labels; when omitted the sidebar renders exactly as before. Prefer this on
125
+ * the wide/full rail and the ⌘K palette on the mobile drawer.
126
+ */
127
+ enableInlineSearch?: boolean;
128
+ /** Pre-localized inline-search strings — required for `enableInlineSearch`. */
129
+ search?: SidebarSearchConfig;
130
+ /** Controlled search query (optional — the sidebar owns the state when omitted). */
131
+ searchQuery?: string;
132
+ /** Called on each query change (with either controlled or internal state). */
133
+ onSearchChange?: (query: string) => void;
72
134
  /** Optional header slot rendered above the items (e.g. a Home shortcut). */
73
135
  header?: React.ReactNode;
74
136
  /** Optional footer slot rendered after a flex spacer (dark-mode toggle, logout). */
@@ -76,7 +138,85 @@ interface SidebarProps {
76
138
  /** Extra container style overrides. */
77
139
  containerStyle?: ViewStyle | ViewStyle[];
78
140
  }
79
- declare const Sidebar: ({ items, pathname, onNavigate, title, regionLabel, navigateHint, expandHint, collapseHint, renderChevron, header, footer, containerStyle, }: SidebarProps) => React.ReactElement;
141
+ declare const Sidebar: ({ items, pathname, onNavigate, title, regionLabel, navigateHint, expandHint, collapseHint, renderChevron, enableInlineSearch, search, searchQuery, onSearchChange, header, footer, containerStyle, }: SidebarProps) => React.ReactElement;
142
+
143
+ /**
144
+ * SidebarChevron — the built-in disclosure caret for an expandable sidebar
145
+ * group. A single "›" glyph that ROTATES from pointing-right (collapsed) to
146
+ * pointing-down (expanded); on web the rotation is a smooth CSS transform tween
147
+ * (reduced-motion snaps). Used as the DEFAULT when a `Sidebar`/`NavExpandableItem`
148
+ * caller supplies no `renderChevron`, so grouped rails get an animated caret for
149
+ * free while callers keep the option to render their own icon set.
150
+ *
151
+ * Package discipline: no icon-set import (the glyph is a plain character); the
152
+ * colour is passed in from the theme by the caller.
153
+ */
154
+
155
+ interface SidebarChevronProps {
156
+ /** Whether the section is expanded (rotates the caret down). */
157
+ expanded: boolean;
158
+ /** Resolved caret colour (from the theme). */
159
+ color: string;
160
+ /** Caret font size (px). */
161
+ size: number;
162
+ }
163
+ /** The default animated disclosure caret for expandable sidebar sections. */
164
+ declare const SidebarChevron: ({ expanded, color, size }: SidebarChevronProps) => React.ReactElement;
165
+
166
+ /**
167
+ * Pure tree-filter for the desktop inline sidebar search. Kept framework-free so
168
+ * the narrowing is unit-testable without rendering. Given a `NavItem[]` tree and
169
+ * a query, it returns a PRUNED tree: a leaf survives when every whitespace-token
170
+ * of the query appears (case-insensitively) in its label; a group survives when
171
+ * it matches itself (kept with ALL its children, so the whole section stays
172
+ * browsable) OR when any descendant survives (kept with only the surviving
173
+ * descendants, so ancestors of a match are never orphaned). An empty/whitespace
174
+ * query returns the tree unchanged — the sidebar shows everything.
175
+ *
176
+ * Requiring ALL tokens lets an operator narrow with "pay dash" the way a fuzzy
177
+ * launcher would, matching the {@link filterCommands} palette semantics so the
178
+ * inline field and the ⌘K palette rank identically.
179
+ */
180
+
181
+ /** Split a raw query into lower-cased match tokens (empty ⇒ no filtering). */
182
+ declare function searchTokens(query: string): string[];
183
+ /**
184
+ * Prune `items` to those matching `query`. Empty query ⇒ a shallow clone of the
185
+ * input (unchanged). A matching group keeps all its children; a non-matching
186
+ * group is kept only if it has surviving descendants (and then carries only
187
+ * those). Order is preserved.
188
+ */
189
+ declare function filterNavItems(items: readonly NavItem[], query: string): NavItem[];
190
+ /** True when the query is non-empty (filtering is active) — drives the empty state. */
191
+ declare function isFilterActive(query: string): boolean;
192
+
193
+ /**
194
+ * The desktop-vs-mobile search decision, split out as a pure rule so it is
195
+ * directly unit-testable. On a WIDE viewport a persistent inline field lives at
196
+ * the top of the sidebar (space is cheap, one keystroke narrows the rail); on a
197
+ * NARROW viewport a persistent field would steal scarce vertical space, so the
198
+ * ⌘K command palette is the search affordance instead. The ⌘K hotkey itself
199
+ * stays bound at every width (see {@link useCommandPaletteHotkey}); this rule
200
+ * only decides which VISIBLE affordance the shell renders.
201
+ *
202
+ * Expressed as a string union (not a `const enum`) to match the package's other
203
+ * public mode types ({@link RailMode}, {@link NavShellLayout}) and to stay safe
204
+ * for the Babel/Expo `isolatedModules` builds of the seven consuming portals,
205
+ * which cannot read a `declare const enum` across the package boundary.
206
+ */
207
+ /** Which search affordance the shell shows at a given viewport. */
208
+ type SearchAffordance = 'inline' | 'palette';
209
+ /**
210
+ * Viewport width (px) at/above which the inline field shows. Mirrors the nav
211
+ * shell's {@link RAIL_FULL_BREAKPOINT} / {@link MOBILE_BREAKPOINT} so the inline
212
+ * field appears exactly where the persistent full rail does.
213
+ */
214
+ declare const DEFAULT_SEARCH_BREAKPOINT = 768;
215
+ /**
216
+ * Resolve the affordance: `'inline'` at/above `breakpoint`, `'palette'` below.
217
+ * `breakpoint` defaults to {@link DEFAULT_SEARCH_BREAKPOINT}.
218
+ */
219
+ declare function resolveSearchAffordance(viewport: number, breakpoint?: number): SearchAffordance;
80
220
 
81
221
  /**
82
222
  * Topbar — the config-driven top navigation bar promoted from the byte-identical
@@ -588,6 +728,19 @@ interface NavShellSideRail {
588
728
  containerStyle?: ViewStyle | ViewStyle[];
589
729
  /** Optional collapsed (icon-only) rail config → the intermediate tablet tier. */
590
730
  collapsed?: NavShellCollapsedRail;
731
+ /**
732
+ * Opt the inner `Sidebar` into its persistent desktop inline-search field.
733
+ * Requires `search` labels; when omitted the rail renders exactly as before.
734
+ * The ⌘K palette remains the search affordance on narrow viewports
735
+ * (see {@link resolveSearchAffordance}). Forwarded to `Sidebar.enableInlineSearch`.
736
+ */
737
+ enableInlineSearch?: boolean;
738
+ /** Pre-localized inline-search strings — required for `enableInlineSearch`. Forwarded to `Sidebar.search`. */
739
+ search?: SidebarSearchConfig;
740
+ /** Controlled search query (optional — the Sidebar owns the state when omitted). Forwarded to `Sidebar.searchQuery`. */
741
+ searchQuery?: string;
742
+ /** Called on each query change (controlled or internal). Forwarded to `Sidebar.onSearchChange`. */
743
+ onSearchChange?: (query: string) => void;
591
744
  }
592
745
  /**
593
746
  * `AppShell` props NavShell forwards straight through. It composes the `header`
@@ -880,6 +1033,14 @@ declare const APP_SHELL_SUFFIX: {
880
1033
  * or an expandable section (toggles nested children). Ported from the twin
881
1034
  * erevna/katalogos `Sidebar/NavExpandableItem`, made config-driven: labels are
882
1035
  * pre-localized strings, icons are render slots, colours come from `useUi`.
1036
+ *
1037
+ * Docs-sidebar polish (v1.16): each row carries a PRIMARY-TINTED overlay whose
1038
+ * opacity eases 0 → hover → active (a smooth ~160ms cross-fade on web, gated by
1039
+ * reduced motion), over the transparent left-accent gutter every leaf reserves —
1040
+ * so the active leaf reads as a tinted pill with a coloured left bar, and any row
1041
+ * lights up subtly on hover, matching the aml-screening docs rail. Expandable
1042
+ * groups get a built-in rotating {@link SidebarChevron} when the caller supplies
1043
+ * no `renderChevron`.
883
1044
  */
884
1045
 
885
1046
  interface NavExpandableItemProps {
@@ -892,7 +1053,11 @@ interface NavExpandableItemProps {
892
1053
  expandHint: string;
893
1054
  /** a11y hint shown when the section is expanded (press collapses). */
894
1055
  collapseHint: string;
895
- /** Optional chevron icon renderer for expandable sections. */
1056
+ /**
1057
+ * Optional chevron icon renderer for expandable sections. When omitted, a
1058
+ * built-in rotating {@link SidebarChevron} is drawn — so grouped rails animate
1059
+ * their disclosure caret without the caller supplying an icon set.
1060
+ */
896
1061
  renderChevron?: (expanded: boolean, color: string, size: number) => React.ReactNode;
897
1062
  depth?: number;
898
1063
  }
@@ -1029,6 +1194,15 @@ declare function roleRoutesToNavItems(routes: RoleRoute[], translate: (key: stri
1029
1194
  declare function accessibleNavItems(user: NavUser, table: RoleRouteTable, translate: (key: string) => string): NavItem[];
1030
1195
 
1031
1196
  declare const ACTIVE_BORDER_RADIUS = 4;
1197
+ /** Corner radius (px) of a sidebar row's hover/active pill — the docs-rail softness. */
1198
+ declare const NAV_ROW_RADIUS = 8;
1199
+ /**
1200
+ * Opacity of the primary-tinted overlay behind the ACTIVE sidebar leaf — a soft
1201
+ * brand pill (the aml-screening docs `.active` fill, flattened from its gradient).
1202
+ */
1203
+ declare const ACTIVE_TINT_OPACITY = 0.14;
1204
+ /** Opacity of the same overlay on HOVER — the subtle docs-rail hover wash. */
1205
+ declare const HOVER_TINT_OPACITY = 0.06;
1032
1206
  /**
1033
1207
  * Horizontal gap (px) between adjacent inline items in the `NavBar` links row.
1034
1208
  * Single source of truth: it is both the row's `columnGap` AND the gap the
@@ -1059,6 +1233,11 @@ declare const navStyles: {
1059
1233
  sidebarSpacer: {
1060
1234
  flex: number;
1061
1235
  };
1236
+ sidebarEmpty: {
1237
+ fontSize: number;
1238
+ paddingVertical: number;
1239
+ paddingHorizontal: number;
1240
+ };
1062
1241
  topbarContainer: {
1063
1242
  height: number;
1064
1243
  paddingHorizontal: number;
@@ -1207,6 +1386,8 @@ declare const collapsedRailStyles: {
1207
1386
  };
1208
1387
  declare const expandableStyles: {
1209
1388
  childItem: {
1389
+ position: "relative";
1390
+ overflow: "hidden";
1210
1391
  borderRadius: number;
1211
1392
  flexDirection: "row";
1212
1393
  alignItems: "center";
@@ -1228,7 +1409,18 @@ declare const expandableStyles: {
1228
1409
  overflow: "hidden";
1229
1410
  marginBottom: number;
1230
1411
  };
1412
+ tintOverlay: {
1413
+ position: "absolute";
1414
+ top: number;
1415
+ left: number;
1416
+ right: number;
1417
+ bottom: number;
1418
+ borderRadius: number;
1419
+ pointerEvents: "none";
1420
+ };
1231
1421
  header: {
1422
+ position: "relative";
1423
+ overflow: "hidden";
1232
1424
  borderRadius: number;
1233
1425
  flexDirection: "row";
1234
1426
  alignItems: "center";
@@ -1261,4 +1453,4 @@ declare const NAV_ICON_SIZE = 14;
1261
1453
  /** Chevron icon size for expandable sections. */
1262
1454
  declare const CHEVRON_ICON_SIZE = 12;
1263
1455
 
1264
- export { ACTIVE_BORDER_RADIUS, APP_SHELL_SUFFIX, type AccountPlan, type AccountState, AppShell, type AppShellProps, type AppShellWidth, BASE_INDENT, CHEVRON_ICON_SIZE, CollapsedRail, type CollapsedRailProps, type CollapsedRailRange, type CommandItem, CommandPalette, type CommandPaletteLabels, type CommandPaletteProps, CommandPaletteTrigger, type CommandPaletteTriggerProps, DEFAULT_COLLAPSED_RAIL_MAX, DarkModeControl, type DarkModeControlProps, type DarkModeOption, type DarkModeVariant, NAV_ICON_SIZE, NAV_LINK_GAP, NAV_TEST_IDS, Nav, NavBar, type NavBarProps, NavExpandableItem, type NavExpandableItemProps, type NavItem, type NavOrientation, NavOverflowMenu, type NavOverflowMenuProps, type NavProps, NavShell, type NavShellCollapsedRail, type NavShellLayout, type NavShellProps, type NavShellSideRail, type NavShellTopBar, type NavUser, PillNav, type PillNavProps, RAIL_FULL_BREAKPOINT, type RailMode, type ResolveRailModeArgs, type ShellMessage, Sidebar, type SidebarProps, Topbar, type TopbarAction, type TopbarProps, type TopbarUser, accessibleNavItems, collapsedRailStyles, darkModeStyles, expandableStyles, filterCommands, isRouteActive, navStyles, pillNavStyles, resolveContentMaxWidth, resolveRailMode, roleRoutesToNavItems, useCommandPaletteHotkey, useContentMaxWidth };
1456
+ export { ACTIVE_BORDER_RADIUS, ACTIVE_TINT_OPACITY, APP_SHELL_SUFFIX, type AccountPlan, type AccountState, AppShell, type AppShellProps, type AppShellWidth, BASE_INDENT, CHEVRON_ICON_SIZE, CollapsedRail, type CollapsedRailProps, type CollapsedRailRange, type CommandItem, CommandPalette, type CommandPaletteLabels, type CommandPaletteProps, CommandPaletteTrigger, type CommandPaletteTriggerProps, DEFAULT_COLLAPSED_RAIL_MAX, DEFAULT_SEARCH_BREAKPOINT, DarkModeControl, type DarkModeControlProps, type DarkModeOption, type DarkModeVariant, HOVER_TINT_OPACITY, NAV_ICON_SIZE, NAV_LINK_GAP, NAV_ROW_RADIUS, NAV_TEST_IDS, Nav, NavBar, type NavBarProps, NavExpandableItem, type NavExpandableItemProps, type NavItem, type NavOrientation, NavOverflowMenu, type NavOverflowMenuProps, type NavProps, NavShell, type NavShellCollapsedRail, type NavShellLayout, type NavShellProps, type NavShellSideRail, type NavShellTopBar, type NavUser, PillNav, type PillNavProps, RAIL_FULL_BREAKPOINT, type RailMode, type ResolveRailModeArgs, type SearchAffordance, type ShellMessage, Sidebar, SidebarChevron, type SidebarChevronProps, type SidebarProps, SidebarSearch, type SidebarSearchConfig, type SidebarSearchLabels, type SidebarSearchProps, Topbar, type TopbarAction, type TopbarProps, type TopbarUser, accessibleNavItems, collapsedRailStyles, darkModeStyles, expandableStyles, filterCommands, filterNavItems, isFilterActive, isRouteActive, navStyles, pillNavStyles, resolveContentMaxWidth, resolveRailMode, resolveSearchAffordance, roleRoutesToNavItems, searchTokens, useCommandPaletteHotkey, useContentMaxWidth };
package/dist/index.d.ts CHANGED
@@ -12,6 +12,45 @@ import { resolveAccessibleRoutes, RoleRouteTable, RoleRoute } from '@dloizides/a
12
12
  */
13
13
  declare function isRouteActive(pathname: string, route: string): boolean;
14
14
 
15
+ /**
16
+ * SidebarSearch — the persistent, DESKTOP inline search field that lives at the
17
+ * top of the {@link Sidebar} and narrows the nav list live as the operator types.
18
+ * On a wide viewport a field is cheap and one keystroke filters the rail; on a
19
+ * narrow viewport the ⌘K {@link CommandPalette} is used instead (a persistent
20
+ * field would steal scarce vertical space) — see {@link resolveSearchAffordance}.
21
+ *
22
+ * Package discipline (as the rest of ui-nav): no FM / router / icon-set imports.
23
+ * The placeholder + a11y strings are pre-localized by the caller; every colour is
24
+ * read from the `@dloizides/ui-feedback` UiProvider theme. A clear ("✕") button
25
+ * appears once there is a query, wired to `onClear`.
26
+ */
27
+
28
+ /** Pre-localized labels/strings for the inline search field. */
29
+ interface SidebarSearchLabels {
30
+ /** Placeholder + accessible name of the input, e.g. "Search". */
31
+ placeholder: string;
32
+ /** a11y hint describing what typing does. */
33
+ hint: string;
34
+ /** Accessible name of the clear ("✕") button. */
35
+ clearLabel: string;
36
+ /** Accessible hint of the clear ("✕") button. */
37
+ clearHint: string;
38
+ }
39
+ interface SidebarSearchProps {
40
+ /** Current query value (controlled). */
41
+ value: string;
42
+ /** Called with the new query on every keystroke. */
43
+ onChangeText: (query: string) => void;
44
+ /** Clear the query (also called by the ✕ button). */
45
+ onClear: () => void;
46
+ /** Pre-localized chrome strings. */
47
+ labels: SidebarSearchLabels;
48
+ /** testID for the input (the clear button is `${testID}-clear`). */
49
+ testID: string;
50
+ }
51
+ /** The inline sidebar search field (desktop) with a clear button. */
52
+ declare const SidebarSearch: ({ value, onChangeText, onClear, labels, testID, }: SidebarSearchProps) => React.ReactElement;
53
+
15
54
  /**
16
55
  * Public prop types for the `@dloizides/ui-nav` config-driven navigation shell.
17
56
  *
@@ -48,8 +87,19 @@ interface NavItem {
48
87
  * supplied `NavItem[]` (leaf + expandable), highlights the active route, and
49
88
  * exposes header/footer slots for app-specific chrome (title, dark-mode toggle,
50
89
  * logout, notification bell). Every colour is read from the UiProvider theme.
90
+ *
91
+ * Desktop inline search (v1.16, opt-in): pass `enableInlineSearch` + `search`
92
+ * labels and a persistent {@link SidebarSearch} field renders under the title and
93
+ * narrows the nav list live (via {@link filterNavItems}). It is OFF by default,
94
+ * so existing consumers — and the mobile drawer, which should use the ⌘K palette
95
+ * instead (see {@link resolveSearchAffordance}) — are byte-identical to before.
51
96
  */
52
97
 
98
+ /** Inline-search chrome strings — the field labels plus the no-match empty text. */
99
+ interface SidebarSearchConfig extends SidebarSearchLabels {
100
+ /** Shown in place of the list when the query matches nothing. */
101
+ emptyText: string;
102
+ }
53
103
  interface SidebarProps {
54
104
  /** Nav entries — already role-filtered / grouped by the app. */
55
105
  items: NavItem[];
@@ -69,6 +119,18 @@ interface SidebarProps {
69
119
  collapseHint?: string;
70
120
  /** Optional chevron renderer for expandable sections. */
71
121
  renderChevron?: (expanded: boolean, color: string, size: number) => React.ReactNode;
122
+ /**
123
+ * Opt into the persistent inline search field (desktop). Requires `search`
124
+ * labels; when omitted the sidebar renders exactly as before. Prefer this on
125
+ * the wide/full rail and the ⌘K palette on the mobile drawer.
126
+ */
127
+ enableInlineSearch?: boolean;
128
+ /** Pre-localized inline-search strings — required for `enableInlineSearch`. */
129
+ search?: SidebarSearchConfig;
130
+ /** Controlled search query (optional — the sidebar owns the state when omitted). */
131
+ searchQuery?: string;
132
+ /** Called on each query change (with either controlled or internal state). */
133
+ onSearchChange?: (query: string) => void;
72
134
  /** Optional header slot rendered above the items (e.g. a Home shortcut). */
73
135
  header?: React.ReactNode;
74
136
  /** Optional footer slot rendered after a flex spacer (dark-mode toggle, logout). */
@@ -76,7 +138,85 @@ interface SidebarProps {
76
138
  /** Extra container style overrides. */
77
139
  containerStyle?: ViewStyle | ViewStyle[];
78
140
  }
79
- declare const Sidebar: ({ items, pathname, onNavigate, title, regionLabel, navigateHint, expandHint, collapseHint, renderChevron, header, footer, containerStyle, }: SidebarProps) => React.ReactElement;
141
+ declare const Sidebar: ({ items, pathname, onNavigate, title, regionLabel, navigateHint, expandHint, collapseHint, renderChevron, enableInlineSearch, search, searchQuery, onSearchChange, header, footer, containerStyle, }: SidebarProps) => React.ReactElement;
142
+
143
+ /**
144
+ * SidebarChevron — the built-in disclosure caret for an expandable sidebar
145
+ * group. A single "›" glyph that ROTATES from pointing-right (collapsed) to
146
+ * pointing-down (expanded); on web the rotation is a smooth CSS transform tween
147
+ * (reduced-motion snaps). Used as the DEFAULT when a `Sidebar`/`NavExpandableItem`
148
+ * caller supplies no `renderChevron`, so grouped rails get an animated caret for
149
+ * free while callers keep the option to render their own icon set.
150
+ *
151
+ * Package discipline: no icon-set import (the glyph is a plain character); the
152
+ * colour is passed in from the theme by the caller.
153
+ */
154
+
155
+ interface SidebarChevronProps {
156
+ /** Whether the section is expanded (rotates the caret down). */
157
+ expanded: boolean;
158
+ /** Resolved caret colour (from the theme). */
159
+ color: string;
160
+ /** Caret font size (px). */
161
+ size: number;
162
+ }
163
+ /** The default animated disclosure caret for expandable sidebar sections. */
164
+ declare const SidebarChevron: ({ expanded, color, size }: SidebarChevronProps) => React.ReactElement;
165
+
166
+ /**
167
+ * Pure tree-filter for the desktop inline sidebar search. Kept framework-free so
168
+ * the narrowing is unit-testable without rendering. Given a `NavItem[]` tree and
169
+ * a query, it returns a PRUNED tree: a leaf survives when every whitespace-token
170
+ * of the query appears (case-insensitively) in its label; a group survives when
171
+ * it matches itself (kept with ALL its children, so the whole section stays
172
+ * browsable) OR when any descendant survives (kept with only the surviving
173
+ * descendants, so ancestors of a match are never orphaned). An empty/whitespace
174
+ * query returns the tree unchanged — the sidebar shows everything.
175
+ *
176
+ * Requiring ALL tokens lets an operator narrow with "pay dash" the way a fuzzy
177
+ * launcher would, matching the {@link filterCommands} palette semantics so the
178
+ * inline field and the ⌘K palette rank identically.
179
+ */
180
+
181
+ /** Split a raw query into lower-cased match tokens (empty ⇒ no filtering). */
182
+ declare function searchTokens(query: string): string[];
183
+ /**
184
+ * Prune `items` to those matching `query`. Empty query ⇒ a shallow clone of the
185
+ * input (unchanged). A matching group keeps all its children; a non-matching
186
+ * group is kept only if it has surviving descendants (and then carries only
187
+ * those). Order is preserved.
188
+ */
189
+ declare function filterNavItems(items: readonly NavItem[], query: string): NavItem[];
190
+ /** True when the query is non-empty (filtering is active) — drives the empty state. */
191
+ declare function isFilterActive(query: string): boolean;
192
+
193
+ /**
194
+ * The desktop-vs-mobile search decision, split out as a pure rule so it is
195
+ * directly unit-testable. On a WIDE viewport a persistent inline field lives at
196
+ * the top of the sidebar (space is cheap, one keystroke narrows the rail); on a
197
+ * NARROW viewport a persistent field would steal scarce vertical space, so the
198
+ * ⌘K command palette is the search affordance instead. The ⌘K hotkey itself
199
+ * stays bound at every width (see {@link useCommandPaletteHotkey}); this rule
200
+ * only decides which VISIBLE affordance the shell renders.
201
+ *
202
+ * Expressed as a string union (not a `const enum`) to match the package's other
203
+ * public mode types ({@link RailMode}, {@link NavShellLayout}) and to stay safe
204
+ * for the Babel/Expo `isolatedModules` builds of the seven consuming portals,
205
+ * which cannot read a `declare const enum` across the package boundary.
206
+ */
207
+ /** Which search affordance the shell shows at a given viewport. */
208
+ type SearchAffordance = 'inline' | 'palette';
209
+ /**
210
+ * Viewport width (px) at/above which the inline field shows. Mirrors the nav
211
+ * shell's {@link RAIL_FULL_BREAKPOINT} / {@link MOBILE_BREAKPOINT} so the inline
212
+ * field appears exactly where the persistent full rail does.
213
+ */
214
+ declare const DEFAULT_SEARCH_BREAKPOINT = 768;
215
+ /**
216
+ * Resolve the affordance: `'inline'` at/above `breakpoint`, `'palette'` below.
217
+ * `breakpoint` defaults to {@link DEFAULT_SEARCH_BREAKPOINT}.
218
+ */
219
+ declare function resolveSearchAffordance(viewport: number, breakpoint?: number): SearchAffordance;
80
220
 
81
221
  /**
82
222
  * Topbar — the config-driven top navigation bar promoted from the byte-identical
@@ -588,6 +728,19 @@ interface NavShellSideRail {
588
728
  containerStyle?: ViewStyle | ViewStyle[];
589
729
  /** Optional collapsed (icon-only) rail config → the intermediate tablet tier. */
590
730
  collapsed?: NavShellCollapsedRail;
731
+ /**
732
+ * Opt the inner `Sidebar` into its persistent desktop inline-search field.
733
+ * Requires `search` labels; when omitted the rail renders exactly as before.
734
+ * The ⌘K palette remains the search affordance on narrow viewports
735
+ * (see {@link resolveSearchAffordance}). Forwarded to `Sidebar.enableInlineSearch`.
736
+ */
737
+ enableInlineSearch?: boolean;
738
+ /** Pre-localized inline-search strings — required for `enableInlineSearch`. Forwarded to `Sidebar.search`. */
739
+ search?: SidebarSearchConfig;
740
+ /** Controlled search query (optional — the Sidebar owns the state when omitted). Forwarded to `Sidebar.searchQuery`. */
741
+ searchQuery?: string;
742
+ /** Called on each query change (controlled or internal). Forwarded to `Sidebar.onSearchChange`. */
743
+ onSearchChange?: (query: string) => void;
591
744
  }
592
745
  /**
593
746
  * `AppShell` props NavShell forwards straight through. It composes the `header`
@@ -880,6 +1033,14 @@ declare const APP_SHELL_SUFFIX: {
880
1033
  * or an expandable section (toggles nested children). Ported from the twin
881
1034
  * erevna/katalogos `Sidebar/NavExpandableItem`, made config-driven: labels are
882
1035
  * pre-localized strings, icons are render slots, colours come from `useUi`.
1036
+ *
1037
+ * Docs-sidebar polish (v1.16): each row carries a PRIMARY-TINTED overlay whose
1038
+ * opacity eases 0 → hover → active (a smooth ~160ms cross-fade on web, gated by
1039
+ * reduced motion), over the transparent left-accent gutter every leaf reserves —
1040
+ * so the active leaf reads as a tinted pill with a coloured left bar, and any row
1041
+ * lights up subtly on hover, matching the aml-screening docs rail. Expandable
1042
+ * groups get a built-in rotating {@link SidebarChevron} when the caller supplies
1043
+ * no `renderChevron`.
883
1044
  */
884
1045
 
885
1046
  interface NavExpandableItemProps {
@@ -892,7 +1053,11 @@ interface NavExpandableItemProps {
892
1053
  expandHint: string;
893
1054
  /** a11y hint shown when the section is expanded (press collapses). */
894
1055
  collapseHint: string;
895
- /** Optional chevron icon renderer for expandable sections. */
1056
+ /**
1057
+ * Optional chevron icon renderer for expandable sections. When omitted, a
1058
+ * built-in rotating {@link SidebarChevron} is drawn — so grouped rails animate
1059
+ * their disclosure caret without the caller supplying an icon set.
1060
+ */
896
1061
  renderChevron?: (expanded: boolean, color: string, size: number) => React.ReactNode;
897
1062
  depth?: number;
898
1063
  }
@@ -1029,6 +1194,15 @@ declare function roleRoutesToNavItems(routes: RoleRoute[], translate: (key: stri
1029
1194
  declare function accessibleNavItems(user: NavUser, table: RoleRouteTable, translate: (key: string) => string): NavItem[];
1030
1195
 
1031
1196
  declare const ACTIVE_BORDER_RADIUS = 4;
1197
+ /** Corner radius (px) of a sidebar row's hover/active pill — the docs-rail softness. */
1198
+ declare const NAV_ROW_RADIUS = 8;
1199
+ /**
1200
+ * Opacity of the primary-tinted overlay behind the ACTIVE sidebar leaf — a soft
1201
+ * brand pill (the aml-screening docs `.active` fill, flattened from its gradient).
1202
+ */
1203
+ declare const ACTIVE_TINT_OPACITY = 0.14;
1204
+ /** Opacity of the same overlay on HOVER — the subtle docs-rail hover wash. */
1205
+ declare const HOVER_TINT_OPACITY = 0.06;
1032
1206
  /**
1033
1207
  * Horizontal gap (px) between adjacent inline items in the `NavBar` links row.
1034
1208
  * Single source of truth: it is both the row's `columnGap` AND the gap the
@@ -1059,6 +1233,11 @@ declare const navStyles: {
1059
1233
  sidebarSpacer: {
1060
1234
  flex: number;
1061
1235
  };
1236
+ sidebarEmpty: {
1237
+ fontSize: number;
1238
+ paddingVertical: number;
1239
+ paddingHorizontal: number;
1240
+ };
1062
1241
  topbarContainer: {
1063
1242
  height: number;
1064
1243
  paddingHorizontal: number;
@@ -1207,6 +1386,8 @@ declare const collapsedRailStyles: {
1207
1386
  };
1208
1387
  declare const expandableStyles: {
1209
1388
  childItem: {
1389
+ position: "relative";
1390
+ overflow: "hidden";
1210
1391
  borderRadius: number;
1211
1392
  flexDirection: "row";
1212
1393
  alignItems: "center";
@@ -1228,7 +1409,18 @@ declare const expandableStyles: {
1228
1409
  overflow: "hidden";
1229
1410
  marginBottom: number;
1230
1411
  };
1412
+ tintOverlay: {
1413
+ position: "absolute";
1414
+ top: number;
1415
+ left: number;
1416
+ right: number;
1417
+ bottom: number;
1418
+ borderRadius: number;
1419
+ pointerEvents: "none";
1420
+ };
1231
1421
  header: {
1422
+ position: "relative";
1423
+ overflow: "hidden";
1232
1424
  borderRadius: number;
1233
1425
  flexDirection: "row";
1234
1426
  alignItems: "center";
@@ -1261,4 +1453,4 @@ declare const NAV_ICON_SIZE = 14;
1261
1453
  /** Chevron icon size for expandable sections. */
1262
1454
  declare const CHEVRON_ICON_SIZE = 12;
1263
1455
 
1264
- export { ACTIVE_BORDER_RADIUS, APP_SHELL_SUFFIX, type AccountPlan, type AccountState, AppShell, type AppShellProps, type AppShellWidth, BASE_INDENT, CHEVRON_ICON_SIZE, CollapsedRail, type CollapsedRailProps, type CollapsedRailRange, type CommandItem, CommandPalette, type CommandPaletteLabels, type CommandPaletteProps, CommandPaletteTrigger, type CommandPaletteTriggerProps, DEFAULT_COLLAPSED_RAIL_MAX, DarkModeControl, type DarkModeControlProps, type DarkModeOption, type DarkModeVariant, NAV_ICON_SIZE, NAV_LINK_GAP, NAV_TEST_IDS, Nav, NavBar, type NavBarProps, NavExpandableItem, type NavExpandableItemProps, type NavItem, type NavOrientation, NavOverflowMenu, type NavOverflowMenuProps, type NavProps, NavShell, type NavShellCollapsedRail, type NavShellLayout, type NavShellProps, type NavShellSideRail, type NavShellTopBar, type NavUser, PillNav, type PillNavProps, RAIL_FULL_BREAKPOINT, type RailMode, type ResolveRailModeArgs, type ShellMessage, Sidebar, type SidebarProps, Topbar, type TopbarAction, type TopbarProps, type TopbarUser, accessibleNavItems, collapsedRailStyles, darkModeStyles, expandableStyles, filterCommands, isRouteActive, navStyles, pillNavStyles, resolveContentMaxWidth, resolveRailMode, roleRoutesToNavItems, useCommandPaletteHotkey, useContentMaxWidth };
1456
+ export { ACTIVE_BORDER_RADIUS, ACTIVE_TINT_OPACITY, APP_SHELL_SUFFIX, type AccountPlan, type AccountState, AppShell, type AppShellProps, type AppShellWidth, BASE_INDENT, CHEVRON_ICON_SIZE, CollapsedRail, type CollapsedRailProps, type CollapsedRailRange, type CommandItem, CommandPalette, type CommandPaletteLabels, type CommandPaletteProps, CommandPaletteTrigger, type CommandPaletteTriggerProps, DEFAULT_COLLAPSED_RAIL_MAX, DEFAULT_SEARCH_BREAKPOINT, DarkModeControl, type DarkModeControlProps, type DarkModeOption, type DarkModeVariant, HOVER_TINT_OPACITY, NAV_ICON_SIZE, NAV_LINK_GAP, NAV_ROW_RADIUS, NAV_TEST_IDS, Nav, NavBar, type NavBarProps, NavExpandableItem, type NavExpandableItemProps, type NavItem, type NavOrientation, NavOverflowMenu, type NavOverflowMenuProps, type NavProps, NavShell, type NavShellCollapsedRail, type NavShellLayout, type NavShellProps, type NavShellSideRail, type NavShellTopBar, type NavUser, PillNav, type PillNavProps, RAIL_FULL_BREAKPOINT, type RailMode, type ResolveRailModeArgs, type SearchAffordance, type ShellMessage, Sidebar, SidebarChevron, type SidebarChevronProps, type SidebarProps, SidebarSearch, type SidebarSearchConfig, type SidebarSearchLabels, type SidebarSearchProps, Topbar, type TopbarAction, type TopbarProps, type TopbarUser, accessibleNavItems, collapsedRailStyles, darkModeStyles, expandableStyles, filterCommands, filterNavItems, isFilterActive, isRouteActive, navStyles, pillNavStyles, resolveContentMaxWidth, resolveRailMode, resolveSearchAffordance, roleRoutesToNavItems, searchTokens, useCommandPaletteHotkey, useContentMaxWidth };