@dloizides/ui-nav 1.11.0 → 1.13.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.js CHANGED
@@ -19,6 +19,7 @@ function isRouteActive(pathname, route) {
19
19
  return pathname === route || pathname.startsWith(`${route}/`);
20
20
  }
21
21
  var ACTIVE_BORDER_RADIUS = 4;
22
+ var ACTIVE_ACCENT_WIDTH = 3;
22
23
  var NAV_LINK_GAP = 4;
23
24
  var navStyles = reactNative.StyleSheet.create({
24
25
  // --- Sidebar ---
@@ -242,23 +243,39 @@ var collapsedRailStyles = reactNative.StyleSheet.create({
242
243
  slot: { alignItems: "center" }
243
244
  });
244
245
  var expandableStyles = reactNative.StyleSheet.create({
246
+ // Every leaf reserves the accent-bar gutter (a TRANSPARENT left border of the
247
+ // same width the active item colours in) so activation never shifts the row.
245
248
  childItem: {
246
- borderRadius: 4,
249
+ borderRadius: 6,
247
250
  flexDirection: "row",
248
251
  alignItems: "center",
249
- paddingVertical: 6
252
+ paddingVertical: 9,
253
+ borderLeftWidth: ACTIVE_ACCENT_WIDTH,
254
+ borderLeftColor: "transparent"
250
255
  },
251
256
  childItemText: { fontSize: 14 },
252
257
  childItemTextWithIcon: { fontSize: 14, marginLeft: 6 },
253
258
  chevron: { marginLeft: "auto" },
254
- childrenContainer: { overflow: "hidden" },
259
+ childrenContainer: { overflow: "hidden", marginBottom: 4 },
255
260
  header: {
256
- borderRadius: 4,
261
+ borderRadius: 6,
257
262
  flexDirection: "row",
258
263
  alignItems: "center",
259
264
  paddingVertical: 8
260
265
  },
266
+ // Depth-0 module groups render as docs-style SECTION HEADERS: extra top rhythm
267
+ // to separate one module from the next.
268
+ sectionHeader: { marginTop: 12 },
261
269
  headerText: { fontSize: 15, fontWeight: "600", marginLeft: 6 },
270
+ // The section-label look: small, uppercased, tracked, muted — the aml-screening
271
+ // docs treatment ("START HERE" / "REFERENCE"), kept on an interactive toggle.
272
+ sectionHeaderText: {
273
+ fontSize: 12,
274
+ fontWeight: "700",
275
+ marginLeft: 6,
276
+ textTransform: "uppercase",
277
+ letterSpacing: 0.8
278
+ },
262
279
  iconWrapper: { width: 20, alignItems: "center" }
263
280
  });
264
281
  var BASE_INDENT = 12;
@@ -289,6 +306,9 @@ function hoverTransitionStyle(reducedMotion) {
289
306
  function ariaCurrentProps(isActive) {
290
307
  return isActive ? { "aria-current": "page" } : {};
291
308
  }
309
+ function ariaExpandedProps(expanded) {
310
+ return { "aria-expanded": expanded };
311
+ }
292
312
  function hasActiveDescendant(item, pathname) {
293
313
  if (isRouteActive(pathname, item.route)) return true;
294
314
  return (item.children ?? []).some((child) => hasActiveDescendant(child, pathname));
@@ -312,9 +332,15 @@ var NavExpandableItem = ({
312
332
  const indent = depth * BASE_INDENT;
313
333
  const hasChildren = Array.isArray(item.children) && item.children.length > 0;
314
334
  const isActive = isRouteActive(pathname, item.route);
335
+ const isSectionHeader = hasChildren && depth === 0;
315
336
  const activeItemStyle = React2.useMemo(
316
- () => ({ backgroundColor: colors.border, borderRadius: ACTIVE_BORDER_RADIUS }),
317
- [colors.border]
337
+ () => ({
338
+ backgroundColor: colors.border,
339
+ borderRadius: ACTIVE_BORDER_RADIUS,
340
+ borderLeftWidth: ACTIVE_ACCENT_WIDTH,
341
+ borderLeftColor: primaryColor
342
+ }),
343
+ [colors.border, primaryColor]
318
344
  );
319
345
  const paddingStyle = React2.useMemo(() => ({ paddingLeft: indent + BASE_INDENT }), [indent]);
320
346
  const headerPaddingStyle = React2.useMemo(() => ({ paddingLeft: indent }), [indent]);
@@ -360,14 +386,29 @@ var NavExpandableItem = ({
360
386
  accessibilityLabel: item.label,
361
387
  accessibilityRole: "button",
362
388
  accessibilityState: { expanded },
363
- style: [expandableStyles.header, headerPaddingStyle, focusRingStyle(focused, primaryColor)],
389
+ style: [
390
+ expandableStyles.header,
391
+ isSectionHeader ? expandableStyles.sectionHeader : void 0,
392
+ headerPaddingStyle,
393
+ focusRingStyle(focused, primaryColor)
394
+ ],
364
395
  testID: item.testID ?? item.key,
365
396
  onBlur: () => setFocused(false),
366
397
  onFocus: () => setFocused(true),
367
398
  onPress: toggle,
399
+ ...ariaExpandedProps(expanded),
368
400
  children: [
369
- typeof item.renderIcon === "function" ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: expandableStyles.iconWrapper, children: item.renderIcon(colors.text, NAV_ICON_SIZE) }) : null,
370
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [expandableStyles.headerText, { color: colors.text }], children: item.label }),
401
+ typeof item.renderIcon === "function" ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: expandableStyles.iconWrapper, children: item.renderIcon(isSectionHeader ? colors.textSecondary : colors.text, NAV_ICON_SIZE) }) : null,
402
+ /* @__PURE__ */ jsxRuntime.jsx(
403
+ reactNative.Text,
404
+ {
405
+ style: [
406
+ isSectionHeader ? expandableStyles.sectionHeaderText : expandableStyles.headerText,
407
+ { color: isSectionHeader ? colors.textSecondary : colors.text }
408
+ ],
409
+ children: item.label
410
+ }
411
+ ),
371
412
  typeof renderChevron === "function" ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: expandableStyles.chevron, children: renderChevron(expanded, colors.textSecondary, CHEVRON_ICON_SIZE) }) : null
372
413
  ]
373
414
  }
@@ -1012,6 +1053,7 @@ var DRAWER_WIDTH = 280;
1012
1053
  var SCRIM_Z_INDEX = 1;
1013
1054
  var PANEL_Z_INDEX = 2;
1014
1055
  var NAV_ACTIVATABLE_SELECTOR = 'a, [role="button"], [role="link"]';
1056
+ var NAV_TOGGLE_SELECTOR = "[aria-expanded]";
1015
1057
  var DEFAULT_DRAWER_LABELS = {
1016
1058
  openLabel: "Menu",
1017
1059
  openHint: "Open the navigation menu",
@@ -1074,8 +1116,10 @@ var MobileDrawer = ({
1074
1116
  if (node === null || typeof node.addEventListener !== "function") return void 0;
1075
1117
  const handleClick = (event) => {
1076
1118
  const target = event.target;
1077
- const onNavItem = target !== null && target.closest(NAV_ACTIVATABLE_SELECTOR) !== null;
1078
- if (onNavItem) onClose();
1119
+ if (target === null) return;
1120
+ const activatable = target.closest(NAV_ACTIVATABLE_SELECTOR);
1121
+ const isToggle = activatable !== null && activatable.closest(NAV_TOGGLE_SELECTOR) !== null;
1122
+ if (activatable !== null && !isToggle) onClose();
1079
1123
  };
1080
1124
  node.addEventListener("click", handleClick);
1081
1125
  return () => node.removeEventListener("click", handleClick);
@@ -1488,6 +1532,208 @@ var DarkModeControl = ({
1488
1532
  }
1489
1533
  );
1490
1534
  };
1535
+
1536
+ // src/commandPaletteFilter.ts
1537
+ function haystack(item) {
1538
+ return [item.label, item.hint ?? "", ...item.keywords ?? []].join(" ").toLowerCase();
1539
+ }
1540
+ function filterCommands(items, query) {
1541
+ const tokens = query.trim().toLowerCase().split(/\s+/).filter(Boolean);
1542
+ if (tokens.length === 0) return [...items];
1543
+ return items.filter((item) => {
1544
+ const hay = haystack(item);
1545
+ return tokens.every((token) => hay.includes(token));
1546
+ });
1547
+ }
1548
+ var SCRIM_COLOR2 = "rgba(0, 0, 0, 0.5)";
1549
+ var OVERLAY_Z_INDEX = 60;
1550
+ var PANEL_MAX_WIDTH = 560;
1551
+ var PANEL_TOP_OFFSET = 88;
1552
+ var LIST_MAX_HEIGHT = 360;
1553
+ var KEY_DOWN = "ArrowDown";
1554
+ var KEY_UP = "ArrowUp";
1555
+ var KEY_ENTER = "Enter";
1556
+ var KEY_ESCAPE = "Escape";
1557
+ var styles4 = reactNative.StyleSheet.create({
1558
+ overlay: { ...reactNative.StyleSheet.absoluteFillObject, zIndex: OVERLAY_Z_INDEX, alignItems: "center" },
1559
+ scrim: { ...reactNative.StyleSheet.absoluteFillObject, backgroundColor: SCRIM_COLOR2 },
1560
+ panel: {
1561
+ marginTop: PANEL_TOP_OFFSET,
1562
+ width: "92%",
1563
+ maxWidth: PANEL_MAX_WIDTH,
1564
+ borderRadius: 12,
1565
+ borderWidth: 1,
1566
+ overflow: "hidden"
1567
+ },
1568
+ input: { fontSize: 16, paddingHorizontal: 16, paddingVertical: 14, borderBottomWidth: 1 },
1569
+ list: { maxHeight: LIST_MAX_HEIGHT },
1570
+ row: { flexDirection: "row", alignItems: "center", paddingHorizontal: 16, paddingVertical: 12, columnGap: 10 },
1571
+ rowText: { flex: 1 },
1572
+ rowLabel: { fontSize: 15, fontWeight: "600" },
1573
+ rowHint: { fontSize: 12, marginTop: 2 },
1574
+ empty: { paddingHorizontal: 16, paddingVertical: 20, fontSize: 14 }
1575
+ });
1576
+ var PaletteRow = ({ item, selected, onChoose }) => {
1577
+ const { theme } = uiFeedback.useUi();
1578
+ const colors = theme.colors;
1579
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1580
+ reactNative.Pressable,
1581
+ {
1582
+ accessibilityHint: item.hint,
1583
+ accessibilityLabel: item.label,
1584
+ accessibilityRole: "button",
1585
+ accessibilityState: { selected },
1586
+ style: [styles4.row, selected ? { backgroundColor: colors.border } : void 0],
1587
+ testID: item.testID ?? item.key,
1588
+ onPress: () => onChoose(item),
1589
+ children: [
1590
+ typeof item.renderIcon === "function" ? item.renderIcon(colors.textSecondary, NAV_ICON_SIZE) : null,
1591
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles4.rowText, children: [
1592
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles4.rowLabel, { color: colors.text }], children: item.label }),
1593
+ item.hint ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles4.rowHint, { color: colors.textSecondary }], children: item.hint }) : null
1594
+ ] })
1595
+ ]
1596
+ }
1597
+ );
1598
+ };
1599
+ var CommandPalette = ({ open, items, onClose, labels, testID }) => {
1600
+ const { theme } = uiFeedback.useUi();
1601
+ const colors = theme.colors;
1602
+ const [query, setQuery] = React2.useState("");
1603
+ const [selected, setSelected] = React2.useState(0);
1604
+ const inputRef = React2.useRef(null);
1605
+ const results = React2.useMemo(() => filterCommands(items, query), [items, query]);
1606
+ React2.useEffect(() => {
1607
+ if (!open) return;
1608
+ setQuery("");
1609
+ setSelected(0);
1610
+ inputRef.current?.focus();
1611
+ }, [open]);
1612
+ React2.useEffect(() => {
1613
+ setSelected((prev) => Math.min(prev, Math.max(0, results.length - 1)));
1614
+ }, [results.length]);
1615
+ const choose = React2.useCallback(
1616
+ (item) => {
1617
+ item.onSelect();
1618
+ onClose();
1619
+ },
1620
+ [onClose]
1621
+ );
1622
+ const onKeyPress = React2.useCallback(
1623
+ (event) => {
1624
+ const key = event.nativeEvent.key;
1625
+ if (key === KEY_DOWN) setSelected((i) => Math.min(i + 1, results.length - 1));
1626
+ else if (key === KEY_UP) setSelected((i) => Math.max(i - 1, 0));
1627
+ else if (key === KEY_ESCAPE) onClose();
1628
+ else if (key === KEY_ENTER && results[selected]) choose(results[selected]);
1629
+ },
1630
+ [results, selected, choose, onClose]
1631
+ );
1632
+ if (!open) return null;
1633
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles4.overlay, children: [
1634
+ /* @__PURE__ */ jsxRuntime.jsx(
1635
+ reactNative.Pressable,
1636
+ {
1637
+ accessibilityHint: labels.closeHint,
1638
+ accessibilityLabel: labels.closeLabel,
1639
+ accessibilityRole: "button",
1640
+ style: styles4.scrim,
1641
+ testID: `${testID ?? "command-palette"}-scrim`,
1642
+ onPress: onClose
1643
+ }
1644
+ ),
1645
+ /* @__PURE__ */ jsxRuntime.jsxs(
1646
+ reactNative.View,
1647
+ {
1648
+ "aria-modal": true,
1649
+ "aria-label": labels.regionLabel,
1650
+ role: "dialog",
1651
+ style: [styles4.panel, { backgroundColor: colors.surface, borderColor: colors.border }],
1652
+ testID: testID ?? "command-palette",
1653
+ children: [
1654
+ /* @__PURE__ */ jsxRuntime.jsx(
1655
+ reactNative.TextInput,
1656
+ {
1657
+ ref: inputRef,
1658
+ accessibilityLabel: labels.placeholder,
1659
+ placeholder: labels.placeholder,
1660
+ placeholderTextColor: colors.textSecondary,
1661
+ style: [styles4.input, { color: colors.text, borderBottomColor: colors.border }],
1662
+ testID: `${testID ?? "command-palette"}-input`,
1663
+ value: query,
1664
+ onChangeText: setQuery,
1665
+ onKeyPress
1666
+ }
1667
+ ),
1668
+ results.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles4.empty, { color: colors.textSecondary }], children: labels.emptyText }) : /* @__PURE__ */ jsxRuntime.jsx(reactNative.ScrollView, { keyboardShouldPersistTaps: "handled", style: styles4.list, children: results.map((item, index) => /* @__PURE__ */ jsxRuntime.jsx(PaletteRow, { item, selected: index === selected, onChoose: choose }, item.key)) })
1669
+ ]
1670
+ }
1671
+ )
1672
+ ] });
1673
+ };
1674
+ var MIN_TARGET = 36;
1675
+ var styles5 = reactNative.StyleSheet.create({
1676
+ trigger: {
1677
+ flexDirection: "row",
1678
+ alignItems: "center",
1679
+ justifyContent: "space-between",
1680
+ columnGap: 8,
1681
+ paddingHorizontal: 12,
1682
+ minHeight: MIN_TARGET,
1683
+ borderWidth: 1,
1684
+ borderRadius: 8
1685
+ },
1686
+ label: { fontSize: 14 },
1687
+ badge: { paddingHorizontal: 6, paddingVertical: 2, borderRadius: 4, borderWidth: 1 },
1688
+ badgeText: { fontSize: 11, fontWeight: "700" }
1689
+ });
1690
+ var CommandPaletteTrigger = ({
1691
+ label,
1692
+ hint,
1693
+ shortcut,
1694
+ onPress,
1695
+ testID
1696
+ }) => {
1697
+ const { theme } = uiFeedback.useUi();
1698
+ const colors = theme.colors;
1699
+ const primary = theme.palette.primary["500"];
1700
+ const [focused, setFocused] = React2__default.default.useState(false);
1701
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1702
+ reactNative.Pressable,
1703
+ {
1704
+ accessibilityHint: hint,
1705
+ accessibilityLabel: label,
1706
+ accessibilityRole: "button",
1707
+ style: [
1708
+ styles5.trigger,
1709
+ { backgroundColor: colors.surface, borderColor: colors.border },
1710
+ focusRingStyle(focused, primary)
1711
+ ],
1712
+ testID,
1713
+ onBlur: () => setFocused(false),
1714
+ onFocus: () => setFocused(true),
1715
+ onPress,
1716
+ children: [
1717
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles5.label, { color: colors.textSecondary }], children: label }),
1718
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles5.badge, { borderColor: colors.border }], children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles5.badgeText, { color: colors.textSecondary }], children: shortcut }) })
1719
+ ]
1720
+ }
1721
+ );
1722
+ };
1723
+ var LAUNCH_KEY = "k";
1724
+ function useCommandPaletteHotkey(onOpen, enabled = true) {
1725
+ React2.useEffect(() => {
1726
+ if (!enabled || typeof document === "undefined") return void 0;
1727
+ const handler = (event) => {
1728
+ const isLauncher = event.key.toLowerCase() === LAUNCH_KEY && (event.metaKey || event.ctrlKey);
1729
+ if (!isLauncher) return;
1730
+ event.preventDefault();
1731
+ onOpen();
1732
+ };
1733
+ document.addEventListener("keydown", handler);
1734
+ return () => document.removeEventListener("keydown", handler);
1735
+ }, [onOpen, enabled]);
1736
+ }
1491
1737
  function roleRoutesToNavItems(routes, translate) {
1492
1738
  return routes.map((entry) => ({
1493
1739
  key: entry.role,
@@ -1505,6 +1751,8 @@ exports.AppShell = AppShell;
1505
1751
  exports.BASE_INDENT = BASE_INDENT;
1506
1752
  exports.CHEVRON_ICON_SIZE = CHEVRON_ICON_SIZE;
1507
1753
  exports.CollapsedRail = CollapsedRail;
1754
+ exports.CommandPalette = CommandPalette;
1755
+ exports.CommandPaletteTrigger = CommandPaletteTrigger;
1508
1756
  exports.DEFAULT_COLLAPSED_RAIL_MAX = DEFAULT_COLLAPSED_RAIL_MAX;
1509
1757
  exports.DarkModeControl = DarkModeControl;
1510
1758
  exports.NAV_ICON_SIZE = NAV_ICON_SIZE;
@@ -1523,12 +1771,14 @@ exports.accessibleNavItems = accessibleNavItems;
1523
1771
  exports.collapsedRailStyles = collapsedRailStyles;
1524
1772
  exports.darkModeStyles = darkModeStyles;
1525
1773
  exports.expandableStyles = expandableStyles;
1774
+ exports.filterCommands = filterCommands;
1526
1775
  exports.isRouteActive = isRouteActive;
1527
1776
  exports.navStyles = navStyles;
1528
1777
  exports.pillNavStyles = pillNavStyles;
1529
1778
  exports.resolveContentMaxWidth = resolveContentMaxWidth;
1530
1779
  exports.resolveRailMode = resolveRailMode;
1531
1780
  exports.roleRoutesToNavItems = roleRoutesToNavItems;
1781
+ exports.useCommandPaletteHotkey = useCommandPaletteHotkey;
1532
1782
  exports.useContentMaxWidth = useContentMaxWidth;
1533
1783
  //# sourceMappingURL=index.js.map
1534
1784
  //# sourceMappingURL=index.js.map