@dloizides/ui-nav 1.10.2 → 1.12.0

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/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { StyleSheet, Platform, TouchableOpacity, View, Text, useWindowDimensions, ActivityIndicator, ScrollView, Pressable } from 'react-native';
1
+ import { StyleSheet, Platform, TouchableOpacity, View, Text, useWindowDimensions, ActivityIndicator, ScrollView, Pressable, TextInput } from 'react-native';
2
2
  import { useUi, UiProvider } from '@dloizides/ui-feedback';
3
3
  import React2, { useState, useCallback, useMemo, useEffect, useRef } from 'react';
4
4
  import { jsxs, jsx } from 'react/jsx-runtime';
@@ -13,6 +13,7 @@ function isRouteActive(pathname, route) {
13
13
  return pathname === route || pathname.startsWith(`${route}/`);
14
14
  }
15
15
  var ACTIVE_BORDER_RADIUS = 4;
16
+ var ACTIVE_ACCENT_WIDTH = 3;
16
17
  var NAV_LINK_GAP = 4;
17
18
  var navStyles = StyleSheet.create({
18
19
  // --- Sidebar ---
@@ -236,23 +237,39 @@ var collapsedRailStyles = StyleSheet.create({
236
237
  slot: { alignItems: "center" }
237
238
  });
238
239
  var expandableStyles = StyleSheet.create({
240
+ // Every leaf reserves the accent-bar gutter (a TRANSPARENT left border of the
241
+ // same width the active item colours in) so activation never shifts the row.
239
242
  childItem: {
240
- borderRadius: 4,
243
+ borderRadius: 6,
241
244
  flexDirection: "row",
242
245
  alignItems: "center",
243
- paddingVertical: 6
246
+ paddingVertical: 9,
247
+ borderLeftWidth: ACTIVE_ACCENT_WIDTH,
248
+ borderLeftColor: "transparent"
244
249
  },
245
250
  childItemText: { fontSize: 14 },
246
251
  childItemTextWithIcon: { fontSize: 14, marginLeft: 6 },
247
252
  chevron: { marginLeft: "auto" },
248
- childrenContainer: { overflow: "hidden" },
253
+ childrenContainer: { overflow: "hidden", marginBottom: 4 },
249
254
  header: {
250
- borderRadius: 4,
255
+ borderRadius: 6,
251
256
  flexDirection: "row",
252
257
  alignItems: "center",
253
258
  paddingVertical: 8
254
259
  },
260
+ // Depth-0 module groups render as docs-style SECTION HEADERS: extra top rhythm
261
+ // to separate one module from the next.
262
+ sectionHeader: { marginTop: 12 },
255
263
  headerText: { fontSize: 15, fontWeight: "600", marginLeft: 6 },
264
+ // The section-label look: small, uppercased, tracked, muted — the aml-screening
265
+ // docs treatment ("START HERE" / "REFERENCE"), kept on an interactive toggle.
266
+ sectionHeaderText: {
267
+ fontSize: 12,
268
+ fontWeight: "700",
269
+ marginLeft: 6,
270
+ textTransform: "uppercase",
271
+ letterSpacing: 0.8
272
+ },
256
273
  iconWrapper: { width: 20, alignItems: "center" }
257
274
  });
258
275
  var BASE_INDENT = 12;
@@ -283,6 +300,13 @@ function hoverTransitionStyle(reducedMotion) {
283
300
  function ariaCurrentProps(isActive) {
284
301
  return isActive ? { "aria-current": "page" } : {};
285
302
  }
303
+ function ariaExpandedProps(expanded) {
304
+ return { "aria-expanded": expanded };
305
+ }
306
+ function hasActiveDescendant(item, pathname) {
307
+ if (isRouteActive(pathname, item.route)) return true;
308
+ return (item.children ?? []).some((child) => hasActiveDescendant(child, pathname));
309
+ }
286
310
  var NavExpandableItem = ({
287
311
  item,
288
312
  pathname,
@@ -293,7 +317,7 @@ var NavExpandableItem = ({
293
317
  renderChevron,
294
318
  depth = 0
295
319
  }) => {
296
- const [expanded, setExpanded] = useState(false);
320
+ const [expanded, setExpanded] = useState(() => hasActiveDescendant(item, pathname));
297
321
  const [focused, setFocused] = useState(false);
298
322
  const { theme } = useUi();
299
323
  const colors = theme.colors;
@@ -302,9 +326,15 @@ var NavExpandableItem = ({
302
326
  const indent = depth * BASE_INDENT;
303
327
  const hasChildren = Array.isArray(item.children) && item.children.length > 0;
304
328
  const isActive = isRouteActive(pathname, item.route);
329
+ const isSectionHeader = hasChildren && depth === 0;
305
330
  const activeItemStyle = useMemo(
306
- () => ({ backgroundColor: colors.border, borderRadius: ACTIVE_BORDER_RADIUS }),
307
- [colors.border]
331
+ () => ({
332
+ backgroundColor: colors.border,
333
+ borderRadius: ACTIVE_BORDER_RADIUS,
334
+ borderLeftWidth: ACTIVE_ACCENT_WIDTH,
335
+ borderLeftColor: primaryColor
336
+ }),
337
+ [colors.border, primaryColor]
308
338
  );
309
339
  const paddingStyle = useMemo(() => ({ paddingLeft: indent + BASE_INDENT }), [indent]);
310
340
  const headerPaddingStyle = useMemo(() => ({ paddingLeft: indent }), [indent]);
@@ -350,14 +380,29 @@ var NavExpandableItem = ({
350
380
  accessibilityLabel: item.label,
351
381
  accessibilityRole: "button",
352
382
  accessibilityState: { expanded },
353
- style: [expandableStyles.header, headerPaddingStyle, focusRingStyle(focused, primaryColor)],
383
+ style: [
384
+ expandableStyles.header,
385
+ isSectionHeader ? expandableStyles.sectionHeader : void 0,
386
+ headerPaddingStyle,
387
+ focusRingStyle(focused, primaryColor)
388
+ ],
354
389
  testID: item.testID ?? item.key,
355
390
  onBlur: () => setFocused(false),
356
391
  onFocus: () => setFocused(true),
357
392
  onPress: toggle,
393
+ ...ariaExpandedProps(expanded),
358
394
  children: [
359
- typeof item.renderIcon === "function" ? /* @__PURE__ */ jsx(View, { style: expandableStyles.iconWrapper, children: item.renderIcon(colors.text, NAV_ICON_SIZE) }) : null,
360
- /* @__PURE__ */ jsx(Text, { style: [expandableStyles.headerText, { color: colors.text }], children: item.label }),
395
+ typeof item.renderIcon === "function" ? /* @__PURE__ */ jsx(View, { style: expandableStyles.iconWrapper, children: item.renderIcon(isSectionHeader ? colors.textSecondary : colors.text, NAV_ICON_SIZE) }) : null,
396
+ /* @__PURE__ */ jsx(
397
+ Text,
398
+ {
399
+ style: [
400
+ isSectionHeader ? expandableStyles.sectionHeaderText : expandableStyles.headerText,
401
+ { color: isSectionHeader ? colors.textSecondary : colors.text }
402
+ ],
403
+ children: item.label
404
+ }
405
+ ),
361
406
  typeof renderChevron === "function" ? /* @__PURE__ */ jsx(View, { style: expandableStyles.chevron, children: renderChevron(expanded, colors.textSecondary, CHEVRON_ICON_SIZE) }) : null
362
407
  ]
363
408
  }
@@ -1002,6 +1047,7 @@ var DRAWER_WIDTH = 280;
1002
1047
  var SCRIM_Z_INDEX = 1;
1003
1048
  var PANEL_Z_INDEX = 2;
1004
1049
  var NAV_ACTIVATABLE_SELECTOR = 'a, [role="button"], [role="link"]';
1050
+ var NAV_TOGGLE_SELECTOR = "[aria-expanded]";
1005
1051
  var DEFAULT_DRAWER_LABELS = {
1006
1052
  openLabel: "Menu",
1007
1053
  openHint: "Open the navigation menu",
@@ -1064,8 +1110,10 @@ var MobileDrawer = ({
1064
1110
  if (node === null || typeof node.addEventListener !== "function") return void 0;
1065
1111
  const handleClick = (event) => {
1066
1112
  const target = event.target;
1067
- const onNavItem = target !== null && target.closest(NAV_ACTIVATABLE_SELECTOR) !== null;
1068
- if (onNavItem) onClose();
1113
+ if (target === null) return;
1114
+ const activatable = target.closest(NAV_ACTIVATABLE_SELECTOR);
1115
+ const isToggle = activatable !== null && activatable.closest(NAV_TOGGLE_SELECTOR) !== null;
1116
+ if (activatable !== null && !isToggle) onClose();
1069
1117
  };
1070
1118
  node.addEventListener("click", handleClick);
1071
1119
  return () => node.removeEventListener("click", handleClick);
@@ -1478,6 +1526,159 @@ var DarkModeControl = ({
1478
1526
  }
1479
1527
  );
1480
1528
  };
1529
+
1530
+ // src/commandPaletteFilter.ts
1531
+ function haystack(item) {
1532
+ return [item.label, item.hint ?? "", ...item.keywords ?? []].join(" ").toLowerCase();
1533
+ }
1534
+ function filterCommands(items, query) {
1535
+ const tokens = query.trim().toLowerCase().split(/\s+/).filter(Boolean);
1536
+ if (tokens.length === 0) return [...items];
1537
+ return items.filter((item) => {
1538
+ const hay = haystack(item);
1539
+ return tokens.every((token) => hay.includes(token));
1540
+ });
1541
+ }
1542
+ var SCRIM_COLOR2 = "rgba(0, 0, 0, 0.5)";
1543
+ var OVERLAY_Z_INDEX = 60;
1544
+ var PANEL_MAX_WIDTH = 560;
1545
+ var PANEL_TOP_OFFSET = 88;
1546
+ var LIST_MAX_HEIGHT = 360;
1547
+ var KEY_DOWN = "ArrowDown";
1548
+ var KEY_UP = "ArrowUp";
1549
+ var KEY_ENTER = "Enter";
1550
+ var KEY_ESCAPE = "Escape";
1551
+ var styles4 = StyleSheet.create({
1552
+ overlay: { ...StyleSheet.absoluteFillObject, zIndex: OVERLAY_Z_INDEX, alignItems: "center" },
1553
+ scrim: { ...StyleSheet.absoluteFillObject, backgroundColor: SCRIM_COLOR2 },
1554
+ panel: {
1555
+ marginTop: PANEL_TOP_OFFSET,
1556
+ width: "92%",
1557
+ maxWidth: PANEL_MAX_WIDTH,
1558
+ borderRadius: 12,
1559
+ borderWidth: 1,
1560
+ overflow: "hidden"
1561
+ },
1562
+ input: { fontSize: 16, paddingHorizontal: 16, paddingVertical: 14, borderBottomWidth: 1 },
1563
+ list: { maxHeight: LIST_MAX_HEIGHT },
1564
+ row: { flexDirection: "row", alignItems: "center", paddingHorizontal: 16, paddingVertical: 12, columnGap: 10 },
1565
+ rowText: { flex: 1 },
1566
+ rowLabel: { fontSize: 15, fontWeight: "600" },
1567
+ rowHint: { fontSize: 12, marginTop: 2 },
1568
+ empty: { paddingHorizontal: 16, paddingVertical: 20, fontSize: 14 }
1569
+ });
1570
+ var PaletteRow = ({ item, selected, onChoose }) => {
1571
+ const { theme } = useUi();
1572
+ const colors = theme.colors;
1573
+ return /* @__PURE__ */ jsxs(
1574
+ Pressable,
1575
+ {
1576
+ accessibilityHint: item.hint,
1577
+ accessibilityLabel: item.label,
1578
+ accessibilityRole: "button",
1579
+ accessibilityState: { selected },
1580
+ style: [styles4.row, selected ? { backgroundColor: colors.border } : void 0],
1581
+ testID: item.testID ?? item.key,
1582
+ onPress: () => onChoose(item),
1583
+ children: [
1584
+ typeof item.renderIcon === "function" ? item.renderIcon(colors.textSecondary, NAV_ICON_SIZE) : null,
1585
+ /* @__PURE__ */ jsxs(View, { style: styles4.rowText, children: [
1586
+ /* @__PURE__ */ jsx(Text, { style: [styles4.rowLabel, { color: colors.text }], children: item.label }),
1587
+ item.hint ? /* @__PURE__ */ jsx(Text, { style: [styles4.rowHint, { color: colors.textSecondary }], children: item.hint }) : null
1588
+ ] })
1589
+ ]
1590
+ }
1591
+ );
1592
+ };
1593
+ var CommandPalette = ({ open, items, onClose, labels, testID }) => {
1594
+ const { theme } = useUi();
1595
+ const colors = theme.colors;
1596
+ const [query, setQuery] = useState("");
1597
+ const [selected, setSelected] = useState(0);
1598
+ const inputRef = useRef(null);
1599
+ const results = useMemo(() => filterCommands(items, query), [items, query]);
1600
+ useEffect(() => {
1601
+ if (!open) return;
1602
+ setQuery("");
1603
+ setSelected(0);
1604
+ inputRef.current?.focus();
1605
+ }, [open]);
1606
+ useEffect(() => {
1607
+ setSelected((prev) => Math.min(prev, Math.max(0, results.length - 1)));
1608
+ }, [results.length]);
1609
+ const choose = useCallback(
1610
+ (item) => {
1611
+ item.onSelect();
1612
+ onClose();
1613
+ },
1614
+ [onClose]
1615
+ );
1616
+ const onKeyPress = useCallback(
1617
+ (event) => {
1618
+ const key = event.nativeEvent.key;
1619
+ if (key === KEY_DOWN) setSelected((i) => Math.min(i + 1, results.length - 1));
1620
+ else if (key === KEY_UP) setSelected((i) => Math.max(i - 1, 0));
1621
+ else if (key === KEY_ESCAPE) onClose();
1622
+ else if (key === KEY_ENTER && results[selected]) choose(results[selected]);
1623
+ },
1624
+ [results, selected, choose, onClose]
1625
+ );
1626
+ if (!open) return null;
1627
+ return /* @__PURE__ */ jsxs(View, { style: styles4.overlay, children: [
1628
+ /* @__PURE__ */ jsx(
1629
+ Pressable,
1630
+ {
1631
+ accessibilityHint: labels.closeHint,
1632
+ accessibilityLabel: labels.closeLabel,
1633
+ accessibilityRole: "button",
1634
+ style: styles4.scrim,
1635
+ testID: `${testID ?? "command-palette"}-scrim`,
1636
+ onPress: onClose
1637
+ }
1638
+ ),
1639
+ /* @__PURE__ */ jsxs(
1640
+ View,
1641
+ {
1642
+ "aria-modal": true,
1643
+ "aria-label": labels.regionLabel,
1644
+ role: "dialog",
1645
+ style: [styles4.panel, { backgroundColor: colors.surface, borderColor: colors.border }],
1646
+ testID: testID ?? "command-palette",
1647
+ children: [
1648
+ /* @__PURE__ */ jsx(
1649
+ TextInput,
1650
+ {
1651
+ ref: inputRef,
1652
+ accessibilityLabel: labels.placeholder,
1653
+ placeholder: labels.placeholder,
1654
+ placeholderTextColor: colors.textSecondary,
1655
+ style: [styles4.input, { color: colors.text, borderBottomColor: colors.border }],
1656
+ testID: `${testID ?? "command-palette"}-input`,
1657
+ value: query,
1658
+ onChangeText: setQuery,
1659
+ onKeyPress
1660
+ }
1661
+ ),
1662
+ results.length === 0 ? /* @__PURE__ */ jsx(Text, { style: [styles4.empty, { color: colors.textSecondary }], children: labels.emptyText }) : /* @__PURE__ */ jsx(ScrollView, { keyboardShouldPersistTaps: "handled", style: styles4.list, children: results.map((item, index) => /* @__PURE__ */ jsx(PaletteRow, { item, selected: index === selected, onChoose: choose }, item.key)) })
1663
+ ]
1664
+ }
1665
+ )
1666
+ ] });
1667
+ };
1668
+ var LAUNCH_KEY = "k";
1669
+ function useCommandPaletteHotkey(onOpen, enabled = true) {
1670
+ useEffect(() => {
1671
+ if (!enabled || typeof document === "undefined") return void 0;
1672
+ const handler = (event) => {
1673
+ const isLauncher = event.key.toLowerCase() === LAUNCH_KEY && (event.metaKey || event.ctrlKey);
1674
+ if (!isLauncher) return;
1675
+ event.preventDefault();
1676
+ onOpen();
1677
+ };
1678
+ document.addEventListener("keydown", handler);
1679
+ return () => document.removeEventListener("keydown", handler);
1680
+ }, [onOpen, enabled]);
1681
+ }
1481
1682
  function roleRoutesToNavItems(routes, translate) {
1482
1683
  return routes.map((entry) => ({
1483
1684
  key: entry.role,
@@ -1489,6 +1690,6 @@ function accessibleNavItems(user, table, translate) {
1489
1690
  return roleRoutesToNavItems(resolveAccessibleRoutes(user, table), translate);
1490
1691
  }
1491
1692
 
1492
- export { ACTIVE_BORDER_RADIUS, APP_SHELL_SUFFIX, AppShell, BASE_INDENT, CHEVRON_ICON_SIZE, CollapsedRail, DEFAULT_COLLAPSED_RAIL_MAX, DarkModeControl, NAV_ICON_SIZE, NAV_LINK_GAP, NAV_TEST_IDS, Nav, NavBar, NavExpandableItem, NavOverflowMenu, NavShell, PillNav, RAIL_FULL_BREAKPOINT, Sidebar, Topbar, accessibleNavItems, collapsedRailStyles, darkModeStyles, expandableStyles, isRouteActive, navStyles, pillNavStyles, resolveContentMaxWidth, resolveRailMode, roleRoutesToNavItems, useContentMaxWidth };
1693
+ export { ACTIVE_BORDER_RADIUS, APP_SHELL_SUFFIX, AppShell, BASE_INDENT, CHEVRON_ICON_SIZE, CollapsedRail, CommandPalette, DEFAULT_COLLAPSED_RAIL_MAX, DarkModeControl, NAV_ICON_SIZE, NAV_LINK_GAP, NAV_TEST_IDS, Nav, NavBar, NavExpandableItem, NavOverflowMenu, NavShell, PillNav, RAIL_FULL_BREAKPOINT, Sidebar, Topbar, accessibleNavItems, collapsedRailStyles, darkModeStyles, expandableStyles, filterCommands, isRouteActive, navStyles, pillNavStyles, resolveContentMaxWidth, resolveRailMode, roleRoutesToNavItems, useCommandPaletteHotkey, useContentMaxWidth };
1493
1694
  //# sourceMappingURL=index.mjs.map
1494
1695
  //# sourceMappingURL=index.mjs.map