pixelize-design-library 2.4.2-beta.60 → 2.4.2-beta.61

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.
Files changed (30) hide show
  1. package/CHANGELOG.md +38 -10
  2. package/COMPONENT-GOTCHAS.md +41 -4
  3. package/DESIGN-GUIDE.md +9 -1
  4. package/dist/Components/NavLauncher/LauncherTabs.d.ts +18 -0
  5. package/dist/Components/NavLauncher/LauncherTabs.js +64 -0
  6. package/dist/Components/NavLauncher/NavLauncher.d.ts +1 -1
  7. package/dist/Components/NavLauncher/NavLauncher.js +203 -53
  8. package/dist/Components/NavLauncher/NavLauncherProps.d.ts +23 -4
  9. package/dist/Components/NavLauncher/NewTabLink.d.ts +2 -2
  10. package/dist/Components/NavLauncher/NewTabLink.js +7 -3
  11. package/dist/Components/NavLauncher/launcherParts.d.ts +4 -1
  12. package/dist/Components/NavLauncher/launcherParts.js +16 -8
  13. package/dist/Components/NavLauncher/resultNavigation.d.ts +3 -0
  14. package/dist/Components/NavLauncher/resultNavigation.js +65 -0
  15. package/dist/Components/NavLauncher/revealStyles.d.ts +1 -1
  16. package/dist/Components/NavLauncher/revealStyles.js +2 -2
  17. package/dist/Components/NavigationBar/NavigationBar.js +2 -1
  18. package/dist/Components/NavigationBar/NavigationBarProps.d.ts +1 -0
  19. package/dist/Components/OrgSwitcher/OrgSwitcher.js +18 -6
  20. package/dist/Components/OrgSwitcher/OrgSwitcherProps.d.ts +4 -0
  21. package/dist/esm/Components/NavLauncher/LauncherTabs.js +58 -0
  22. package/dist/esm/Components/NavLauncher/NavLauncher.js +204 -54
  23. package/dist/esm/Components/NavLauncher/NewTabLink.js +4 -3
  24. package/dist/esm/Components/NavLauncher/launcherParts.js +15 -8
  25. package/dist/esm/Components/NavLauncher/resultNavigation.js +61 -0
  26. package/dist/esm/Components/NavLauncher/revealStyles.js +2 -2
  27. package/dist/esm/Components/NavigationBar/NavigationBar.js +2 -1
  28. package/dist/esm/Components/OrgSwitcher/OrgSwitcher.js +16 -7
  29. package/dist/index.d.ts +1 -0
  30. package/package.json +1 -1
@@ -1,12 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { Box, Flex, Heading, IconButton, Text } from "@chakra-ui/react";
3
- import { Compass, Search as SearchIcon, X } from "lucide-react";
2
+ import { Box, Flex, IconButton, Text, VisuallyHidden } from "@chakra-ui/react";
3
+ import { Search as SearchIcon, X } from "lucide-react";
4
4
  import { useCallback, useEffect, useId, useMemo, useRef, useState, } from "react";
5
5
  import { useMatchMedia } from "../../Hooks/useMatchMedia.js";
6
6
  import { focusVisibleStyles } from "../../Theme/chakra/focusRing.styles.js";
7
+ import { reducedMotionSx } from "../../Theme/chakra/motion.js";
7
8
  import { chartMode } from "../../Theme/tokens/builders/chartColorsFromTheme.js";
8
9
  import { placeholderInk } from "../../Theme/tokens/builders/placeholderInk.js";
9
- import { resolveFilledSurface } from "../../Theme/tokens/builders/filledSurface.js";
10
10
  import { listRowBackgrounds } from "../../Theme/tokens/builders/rowStates.js";
11
11
  import { transitionFor } from "../../Theme/tokens/primitives/motion.js";
12
12
  import { useCustomTheme } from "../../Theme/useCustomTheme.js";
@@ -18,12 +18,15 @@ import ToolTip from "../ToolTip/ToolTip.js";
18
18
  import { useIsTruncated } from "../../Hooks/useIsTruncated.js";
19
19
  import { shortcutKeys } from "../AppSwitcher/AppSwitcherProps.js";
20
20
  import Skeletons from "../Skeletons/Skeletons.js";
21
- import { activeRowSurface, AppsSection, CountBadge, Highlight, isPlainClick, ITEM_SELECTOR, ModuleRow, NewTag, PinnedTile, PinTextButton, RailRow, ROW_CONTAINER, RowActions, RowIcon, RowTip, SectionHeader, titleInset, WIDE_ROW, } from "./launcherParts.js";
21
+ import { activeRowSurface, AppsSection, CountBadge, Highlight, isPlainClick, ITEM_SELECTOR, ModuleRow, NewTag, PinnedTile, PinTextButton, RailRow, ROW_CONTAINER, RowActions, RowIcon, RowTip, SectionHeader, titleInset, TouchDensity, useTouchDensity, WIDE_ROW, } from "./launcherParts.js";
22
+ import { LauncherTabs, panelId, tabId, } from "./LauncherTabs.js";
22
23
  import { searchLauncherSections } from "./match.js";
23
24
  import NewTabLink from "./NewTabLink.js";
24
25
  import PinToggle from "./PinToggle.js";
26
+ import { moveResult } from "./resultNavigation.js";
25
27
  const NO_APPS = [];
26
28
  const MD_QUERY = "(min-width: 48em)";
29
+ const LG_QUERY = "(min-width: 62em)";
27
30
  const XL_QUERY = "(min-width: 80em)";
28
31
  const CELL_SELECTOR = "[data-nav-cell]";
29
32
  const RAIL_WIDTH = "clamp(20rem, 30%, 26rem)";
@@ -31,6 +34,19 @@ const PINNED_COLUMNS = 2;
31
34
  const ENTER_CHIP_WIDTH = 50;
32
35
  const ARROW_KEYS = ["↑", "↓", "←", "→"];
33
36
  const ENTER_KEY = shortcutKeys("enter")[0];
37
+ const MOD_KEY = shortcutKeys("mod+enter")[0];
38
+ const RESULT_KEYS = [
39
+ "ArrowUp",
40
+ "ArrowDown",
41
+ "ArrowLeft",
42
+ "ArrowRight",
43
+ "Home",
44
+ "End",
45
+ "PageUp",
46
+ "PageDown",
47
+ ];
48
+ const TEXT_TARGET = "input, textarea, select, [contenteditable=true]";
49
+ const ROW_HEIGHT = 46;
34
50
  const DEFAULT_LABELS = {
35
51
  title: "Menu",
36
52
  searchPlaceholder: "Search menus and screens",
@@ -56,6 +72,13 @@ const DEFAULT_LABELS = {
56
72
  hintPin: "pin",
57
73
  hintClose: "close",
58
74
  hintOpenMenu: "open",
75
+ hintClickNewTab: "+ click for new tab",
76
+ newTabTip: "Open in new tab",
77
+ resultsCount: (count) => `${count} ${count === 1 ? "result" : "results"}`,
78
+ tabsLabel: "Menu sections",
79
+ tabMenu: "Menu",
80
+ tabQuickAccess: "Quick access",
81
+ tabApps: "Apps",
59
82
  apps: "Your apps",
60
83
  found: (count) => `${count} found`,
61
84
  pinTip: "Pin to quick access",
@@ -92,19 +115,19 @@ const GRID_COLUMNS = {
92
115
  function ItemGrid({ children, columns, }) {
93
116
  return (_jsx(Box, { "data-nav-grid": true, "data-nav-columns": columns, display: "grid", gridTemplateColumns: GRID_COLUMNS, gap: "1px 8px", alignItems: "start", children: children }));
94
117
  }
95
- function Pane({ children }) {
96
- return (_jsx(Flex, { direction: "column", gap: "14px", flex: 1, minW: 0, px: "18px", pt: "12px", pb: "16px", overflowY: { base: "visible", lg: "auto" }, children: children }));
118
+ function Pane({ children, scrolls = false, }) {
119
+ return (_jsx(Flex, { "data-scroll-region": scrolls || undefined, direction: "column", gap: "14px", flex: 1, minW: 0, px: "18px", pt: "12px", pb: "16px", overflowY: { base: "visible", lg: "auto" }, children: children }));
97
120
  }
98
- function Rail({ label, children }) {
121
+ function Rail({ label, children, scrolls = false, }) {
99
122
  const { colors } = useCustomTheme();
100
- return (_jsx(Flex, { as: "aside", "aria-label": label, direction: "column", gap: "16px", w: { base: "100%", lg: RAIL_WIDTH }, flexShrink: 0, px: "14px", pt: "12px", pb: "16px", bg: colors.backgroundColor.subtle, borderTopWidth: { base: "1px", lg: 0 }, borderLeftWidth: { base: 0, lg: "1px" }, borderColor: colors.boxborder[200], overflowY: { base: "visible", lg: "auto" }, children: children }));
123
+ return (_jsx(Flex, { "data-scroll-region": scrolls || undefined, as: "aside", "aria-label": label, direction: "column", gap: "16px", w: { base: "100%", lg: RAIL_WIDTH }, flexShrink: 0, px: "14px", pt: "12px", pb: "16px", bg: colors.backgroundColor.subtle, borderTopWidth: { base: "1px", lg: 0 }, borderLeftWidth: { base: 0, lg: "1px" }, borderColor: colors.boxborder[200], overflowY: { base: "visible", lg: "auto" }, children: children }));
101
124
  }
102
125
  function SkeletonLayout({ label }) {
103
126
  return (_jsxs(Flex, { role: "status", "aria-busy": "true", "aria-label": label, direction: { base: "column", lg: "row" }, flex: 1, minH: 0, children: [_jsx(Pane, { children: [0, 1, 2].map((section) => (_jsxs(Box, { children: [_jsx(Skeletons, { type: "skeleton", height: "0.75rem", width: "6rem" }), _jsx(ItemGrid, { children: [0, 1, 2, 3].map((row) => (_jsxs(Flex, { align: "center", gap: "10px", h: "34px", mt: 2, children: [_jsx(Skeletons, { type: "skeleton", height: "1rem", width: "1rem" }), _jsx(Skeletons, { type: "skeleton", height: "0.875rem", width: "8rem" })] }, row))) })] }, section))) }), _jsx(Rail, { label: label, children: [0, 1].map((group) => (_jsxs(Box, { children: [_jsx(Skeletons, { type: "skeleton", height: "0.75rem", width: "5rem" }), [0, 1, 2].map((row) => (_jsx(Skeletons, { type: "skeleton", height: "1.5rem", width: "100%", mt: 2 }, row)))] }, group))) })] }));
104
127
  }
105
128
  function SearchField({ inputRef, value, onChange, onKeyDown, labels, hint, listboxId, activeId, isExpanded, hasResults, }) {
106
129
  const { colors } = useCustomTheme();
107
- return (_jsxs(Flex, { flex: 1, maxW: "520px", h: "38px", align: "center", gap: "10px", pl: "12px", pr: "8px", borderRadius: "10px", borderWidth: "1px", borderColor: colors.primary[500], bg: colors.surfaceRaised, boxShadow: `0 0 0 3px ${colors.primary.opacity[16]}`, children: [_jsx(Box, { as: "span", color: colors.accentText, display: "inline-flex", flexShrink: 0, children: _jsx(SearchIcon, { size: 16, strokeWidth: 2.2, "aria-hidden": true }) }), _jsx(Box, { as: "input", ref: inputRef, value: value, onChange: (event) => onChange(event.target.value), onKeyDown: onKeyDown, placeholder: labels.searchPlaceholder, "aria-label": labels.searchPlaceholder, role: "combobox", "aria-expanded": isExpanded, "aria-controls": isExpanded && hasResults ? listboxId : undefined, "aria-autocomplete": "list", "aria-activedescendant": activeId, autoComplete: "off", flex: 1, minW: 0, h: "34px", border: 0, outline: "none", bg: "transparent", fontSize: "14.5px", color: colors.text[900], _placeholder: { color: placeholderInk(colors), opacity: 1 } }), value ? (_jsx(Box, { as: "button", type: "button", "aria-label": labels.clearSearch, onClick: () => {
130
+ return (_jsxs(Flex, { flex: 1, minW: 0, h: { base: "40px", md: "44px" }, align: "center", gap: "10px", pl: "14px", pr: "10px", borderRadius: { base: "10px", md: "12px" }, borderWidth: "1px", borderColor: colors.primary[500], bg: colors.surfaceRaised, boxShadow: `0 0 0 3px ${colors.primary.opacity[16]}`, children: [_jsx(Box, { as: "span", color: colors.accentText, display: "inline-flex", flexShrink: 0, children: _jsx(SearchIcon, { size: 18, strokeWidth: 2.2, "aria-hidden": true }) }), _jsx(Box, { as: "input", ref: inputRef, value: value, onChange: (event) => onChange(event.target.value), onKeyDown: onKeyDown, placeholder: labels.searchPlaceholder, "aria-label": labels.searchPlaceholder, role: "combobox", "aria-expanded": isExpanded, "aria-controls": isExpanded && hasResults ? listboxId : undefined, "aria-autocomplete": "list", "aria-activedescendant": activeId, autoComplete: "off", flex: 1, minW: 0, h: "100%", border: 0, outline: "none", bg: "transparent", fontSize: "16px", color: colors.text[900], _placeholder: { color: placeholderInk(colors), opacity: 1 } }), value ? (_jsx(Box, { as: "button", type: "button", "aria-label": labels.clearSearch, onClick: () => {
108
131
  var _a;
109
132
  onChange("");
110
133
  (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
@@ -112,6 +135,7 @@ function SearchField({ inputRef, value, onChange, onKeyDown, labels, hint, listb
112
135
  }
113
136
  function SearchRow({ item, query, isActive, link, labels, optionId, isPinned, canPin, onPin, onOpen, onHover, }) {
114
137
  const { colors } = useCustomTheme();
138
+ const isTouch = useTouchDensity();
115
139
  const isScreen = item.kind === "screen";
116
140
  const hasStar = Boolean(item.screenId) && canPin;
117
141
  const base = item.badge ? 24 : 0;
@@ -122,7 +146,7 @@ function SearchRow({ item, query, isActive, link, labels, optionId, isPinned, ca
122
146
  const titleRef = title.ref;
123
147
  const contextRef = context.ref;
124
148
  const tipLabel = isScreen || !item.context ? fullLabel : `${fullLabel} · ${item.context}`;
125
- return (_jsx(ToolTip, { label: tipLabel, placement: "top-start", isDisabled: !title.isTruncated && !context.isTruncated, children: _jsxs(Flex, { role: "presentation", "data-group": true, "data-reveal": isActive, position: "relative", align: "flex-start", gap: "10px", minH: "34px", pl: "8px", pr: "4px", pt: "7px", pb: "5px", borderRadius: "8px", transition: transitionFor(["background-color"]), onMouseMove: onHover, sx: isActive
149
+ return (_jsx(ToolTip, { label: tipLabel, placement: "top-start", isDisabled: !title.isTruncated && !context.isTruncated, children: _jsxs(Flex, { role: "presentation", "data-group": true, "data-reveal": isActive, position: "relative", align: "flex-start", gap: "10px", minH: isTouch ? "44px" : "34px", pl: "8px", pr: "4px", pt: isTouch ? "12px" : "7px", pb: isTouch ? "8px" : "5px", borderRadius: "8px", transition: transitionFor(["background-color"]), onMouseMove: onHover, sx: isActive
126
150
  ? { ...ROW_CONTAINER, ...activeRowSurface(colors) }
127
151
  : ROW_CONTAINER, children: [_jsxs(Flex, { id: optionId, role: "option", "aria-selected": isActive, "data-active": isActive, flex: 1, minW: 0, align: "flex-start", gap: "10px", cursor: "pointer", onClick: () => {
128
152
  if (!item.href)
@@ -131,20 +155,23 @@ function SearchRow({ item, query, isActive, link, labels, optionId, isPinned, ca
131
155
  display: "none",
132
156
  [WIDE_ROW]: { display: "inline-flex" },
133
157
  "@media (hover: none)": { display: "none" },
134
- }, children: _jsx(KbdShortcut, { shortcut: "enter", size: "xs" }) })) : null, _jsx(CountBadge, { value: item.badge }), hasStar ? (_jsx(PinToggle, { isPinned: isPinned, label: isPinned ? labels.unpin(item.label) : labels.pin(item.label), title: "", onToggle: onPin, tabIndex: -1 })) : null, item.href ? (_jsx(NewTabLink, { href: item.href, label: labels.openInNewTab(isScreen ? `${item.label} — ${item.context}` : item.label), linkAs: link.linkAs, tabIndex: -1 })) : null] }) }), hasStar ? (_jsx(RowTip, { text: isPinned ? labels.unpinTip : labels.pinTip })) : null] }) }));
158
+ }, children: _jsx(KbdShortcut, { shortcut: "enter", size: "xs" }) })) : null, _jsx(CountBadge, { value: item.badge }), hasStar ? (_jsx(PinToggle, { isPinned: isPinned, label: isPinned ? labels.unpin(item.label) : labels.pin(item.label), title: "", onToggle: onPin, tabIndex: -1 })) : null, item.href ? (_jsx(NewTabLink, { href: item.href, label: labels.openInNewTab(isScreen ? `${item.label} — ${item.context}` : item.label), tip: labels.newTabTip, linkAs: link.linkAs, tabIndex: -1 })) : null] }) }), hasStar ? (_jsx(RowTip, { text: isPinned ? labels.unpinTip : labels.pinTip })) : null] }) }));
135
159
  }
136
160
  /** Left-drawer navigation launcher: browse every module, pins, Frequent, Recent, search and (compact) the suite's apps. */
137
- export default function NavLauncher({ isOpen, onClose, sections: rawSections, pinnedIds, frequentIds, recentIds, onTogglePin, onNavigate, linkAs = "a", actions, extraResults, query: controlledQuery, onQueryChange, isSearching = false, isLoading = false, keyScreenLimit = 3, frequentLimit = 4, suggestions, searchHint, openShortcutHint, modKeyLabel, maxWidth = "1080px", labels: labelOverrides, apps, currentAppId, onAppSelect, appsVisibility = "apps-on-compact", "data-testid": testId, }) {
161
+ export default function NavLauncher({ isOpen, onClose, sections: rawSections, pinnedIds, frequentIds, recentIds, onTogglePin, onNavigate, linkAs = "a", actions, extraResults, query: controlledQuery, onQueryChange, isSearching = false, isLoading = false, keyScreenLimit = 3, frequentLimit = 4, suggestions, searchHint, modKeyLabel, headerActions, maxWidth = "1080px", labels: labelOverrides, apps, currentAppId, onAppSelect, appsVisibility = "apps-on-compact", "data-testid": testId, }) {
138
162
  const theme = useCustomTheme();
139
163
  const { colors } = theme;
140
164
  const inputRef = useRef(null);
141
165
  const bodyRef = useRef(null);
142
166
  const baseId = useId();
143
167
  const isDesktop = useMatchMedia(MD_QUERY, true);
168
+ const isLg = useMatchMedia(LG_QUERY, true);
144
169
  const isWide = useMatchMedia(XL_QUERY, false);
145
170
  const columns = isWide ? 3 : isDesktop ? 2 : 1;
146
171
  const [innerQuery, setInnerQuery] = useState("");
147
172
  const [activeIndex, setActiveIndex] = useState(0);
173
+ const [tab, setTab] = useState("menu");
174
+ const navigating = useRef(false);
148
175
  const labels = useMemo(() => ({ ...DEFAULT_LABELS, ...labelOverrides }), [labelOverrides]);
149
176
  const isControlled = controlledQuery !== undefined;
150
177
  const query = isControlled ? controlledQuery : innerQuery;
@@ -156,10 +183,16 @@ export default function NavLauncher({ isOpen, onClose, sections: rawSections, pi
156
183
  const latest = useRef({ query, setQuery });
157
184
  latest.current = { query, setQuery };
158
185
  useEffect(() => {
159
- if (!isOpen && latest.current.query !== "")
186
+ if (isOpen)
187
+ return;
188
+ setTab("menu");
189
+ if (latest.current.query !== "")
160
190
  latest.current.setQuery("");
161
191
  }, [isOpen]);
162
- useEffect(() => setActiveIndex(0), [query]);
192
+ useEffect(() => {
193
+ navigating.current = false;
194
+ setActiveIndex(0);
195
+ }, [query]);
163
196
  const sections = useMemo(() => visibleSections(rawSections), [rawSections]);
164
197
  const byId = useMemo(() => indexScreens(sections), [sections]);
165
198
  const pinned = useMemo(() => new Set(pinnedIds), [pinnedIds]);
@@ -186,6 +219,26 @@ export default function NavLauncher({ isOpen, onClose, sections: rawSections, pi
186
219
  const groupStarts = useMemo(() => groups.map((_, i) => groups.slice(0, i).reduce((n, g) => n + g.items.length, 0)), [groups]);
187
220
  const isSearchView = query.trim().length > 0;
188
221
  const active = Math.min(activeIndex, Math.max(results.length - 1, 0));
222
+ const appList = apps !== null && apps !== void 0 ? apps : NO_APPS;
223
+ const showApps = appList.some((app) => app.id !== currentAppId) &&
224
+ (appsVisibility === "always" || !isDesktop);
225
+ const railFrequent = frequentItems
226
+ .filter(({ screen }) => !pinned.has(screen.id))
227
+ .slice(0, frequentLimit);
228
+ const hasQuick = pinnedItems.length > 0 || railFrequent.length > 0 || recentItems.length > 0;
229
+ const tabs = useMemo(() => [
230
+ { id: "menu", label: labels.tabMenu },
231
+ ...(hasQuick
232
+ ? [{ id: "quick", label: labels.tabQuickAccess }]
233
+ : []),
234
+ ...(showApps ? [{ id: "apps", label: labels.tabApps }] : []),
235
+ ], [hasQuick, showApps, labels]);
236
+ const isTabbed = !isLg && !isSearchView && !isLoading && tabs.length > 1;
237
+ const activeTab = tabs.some((item) => item.id === tab) ? tab : "menu";
238
+ const countText = isSearchView && results.length > 0
239
+ ? labels.resultsCount(results.length)
240
+ : "";
241
+ const groupSizes = useMemo(() => groups.map((g) => g.items.length), [groups]);
189
242
  const link = useMemo(() => ({
190
243
  linkAs,
191
244
  onPlainClick: (event) => {
@@ -205,14 +258,31 @@ export default function NavLauncher({ isOpen, onClose, sections: rawSections, pi
205
258
  onClose();
206
259
  onNavigate(item.href, { newTab });
207
260
  }, [onClose, onNavigate]);
261
+ const focusField = (shouldSelect = false) => {
262
+ var _a, _b;
263
+ (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
264
+ if (shouldSelect)
265
+ (_b = inputRef.current) === null || _b === void 0 ? void 0 : _b.select();
266
+ };
267
+ const scrollHeight = (from) => {
268
+ var _a, _b, _c, _d, _e;
269
+ return (_e = (_b = (_a = from === null || from === void 0 ? void 0 : from.closest("[data-scroll-region]")) === null || _a === void 0 ? void 0 : _a.clientHeight) !== null && _b !== void 0 ? _b : (_d = (_c = bodyRef.current) === null || _c === void 0 ? void 0 : _c.querySelector("[data-scroll-region]")) === null || _d === void 0 ? void 0 : _d.clientHeight) !== null && _e !== void 0 ? _e : 0;
270
+ };
271
+ const leaveTop = () => {
272
+ var _a;
273
+ const selected = isTabbed
274
+ ? document.getElementById(tabId(baseId, activeTab))
275
+ : null;
276
+ (_a = (selected !== null && selected !== void 0 ? selected : inputRef.current)) === null || _a === void 0 ? void 0 : _a.focus();
277
+ };
208
278
  const moveFocus = (delta) => {
209
- var _a, _b, _c;
279
+ var _a, _b;
210
280
  const items = Array.from((_b = (_a = bodyRef.current) === null || _a === void 0 ? void 0 : _a.querySelectorAll(ITEM_SELECTOR)) !== null && _b !== void 0 ? _b : []);
211
281
  if (items.length === 0)
212
282
  return;
213
283
  const current = items.indexOf(document.activeElement);
214
284
  if (typeof delta === "number" && current + delta < 0) {
215
- (_c = inputRef.current) === null || _c === void 0 ? void 0 : _c.focus();
285
+ leaveTop();
216
286
  return;
217
287
  }
218
288
  const next = delta === "first"
@@ -222,6 +292,22 @@ export default function NavLauncher({ isOpen, onClose, sections: rawSections, pi
222
292
  : Math.min(items.length - 1, current + delta);
223
293
  items[next].focus();
224
294
  };
295
+ const pageFocus = (direction) => {
296
+ var _a, _b;
297
+ const items = Array.from((_b = (_a = bodyRef.current) === null || _a === void 0 ? void 0 : _a.querySelectorAll(ITEM_SELECTOR)) !== null && _b !== void 0 ? _b : []);
298
+ const from = document.activeElement;
299
+ const current = items.indexOf(from);
300
+ if (items.length === 0 || current < 0)
301
+ return;
302
+ const height = scrollHeight(from);
303
+ const goal = from.getBoundingClientRect().top + direction * height * 0.85;
304
+ const byDistance = items
305
+ .filter((_, i) => (direction > 0 ? i > current : i < current))
306
+ .sort((a, b) => Math.abs(a.getBoundingClientRect().top - goal) -
307
+ Math.abs(b.getBoundingClientRect().top - goal));
308
+ const fallback = items[Math.min(items.length - 1, Math.max(0, current + direction * 3))];
309
+ (height > 0 && byDistance.length ? byDistance[0] : fallback).focus();
310
+ };
225
311
  const onInputKeyDown = (event) => {
226
312
  if (!isSearchView) {
227
313
  if (event.key === "ArrowDown") {
@@ -230,20 +316,32 @@ export default function NavLauncher({ isOpen, onClose, sections: rawSections, pi
230
316
  }
231
317
  return;
232
318
  }
233
- if (results.length === 0)
319
+ if (results.length === 0 || event.nativeEvent.isComposing)
234
320
  return;
235
- if (event.key === "ArrowDown") {
236
- event.preventDefault();
237
- setActiveIndex((active + 1) % results.length);
238
- }
239
- else if (event.key === "ArrowUp") {
240
- event.preventDefault();
241
- setActiveIndex((active - 1 + results.length) % results.length);
242
- }
243
- else if (event.key === "Enter" && !event.nativeEvent.isComposing) {
321
+ if (event.key === "Enter") {
244
322
  event.preventDefault();
245
323
  open(results[active], event.metaKey || event.ctrlKey);
324
+ return;
325
+ }
326
+ const hasModifier = event.altKey || event.metaKey || event.ctrlKey || event.shiftKey;
327
+ if (!RESULT_KEYS.includes(event.key) || hasModifier)
328
+ return;
329
+ const input = event.currentTarget;
330
+ const atEnd = input.selectionStart === input.value.length &&
331
+ input.selectionEnd === input.value.length;
332
+ const isSideways = event.key === "ArrowLeft" || event.key === "ArrowRight";
333
+ const isEdit = isSideways || event.key === "Home" || event.key === "End";
334
+ if (isEdit && !navigating.current) {
335
+ const entersGrid = event.key === "ArrowRight" && atEnd && columns > 1;
336
+ if (!entersGrid)
337
+ return;
246
338
  }
339
+ if (isSideways && columns === 1)
340
+ return;
341
+ event.preventDefault();
342
+ navigating.current = true;
343
+ const pageRows = Math.max(3, Math.floor(scrollHeight(input) / ROW_HEIGHT) - 1);
344
+ setActiveIndex(moveResult(groupSizes, columns, active, event.key, pageRows));
247
345
  };
248
346
  const moveInGrid = (event, cell) => {
249
347
  var _a, _b, _c, _d;
@@ -295,27 +393,39 @@ export default function NavLauncher({ isOpen, onClose, sections: rawSections, pi
295
393
  return true;
296
394
  };
297
395
  const onBodyKeyDown = (event) => {
298
- var _a, _b;
299
- if (event.target === inputRef.current)
396
+ var _a;
397
+ const target = event.target;
398
+ if (target.closest(TEXT_TARGET))
300
399
  return;
400
+ const inBody = Boolean((_a = bodyRef.current) === null || _a === void 0 ? void 0 : _a.contains(target));
301
401
  const isArrow = event.key.startsWith("Arrow");
302
- const cell = event.target.closest(CELL_SELECTOR);
303
- if (isArrow && cell && !event.altKey && !event.metaKey && !event.ctrlKey) {
402
+ const cell = target.closest(CELL_SELECTOR);
403
+ if (inBody &&
404
+ isArrow &&
405
+ cell &&
406
+ !event.altKey &&
407
+ !event.metaKey &&
408
+ !event.ctrlKey) {
304
409
  event.preventDefault();
305
410
  moveInGrid(event, cell);
306
411
  }
307
- else if (event.key === "ArrowDown" || event.key === "ArrowUp") {
412
+ else if (inBody &&
413
+ (event.key === "ArrowDown" || event.key === "ArrowUp")) {
308
414
  event.preventDefault();
309
415
  moveFocus(event.key === "ArrowDown" ? 1 : -1);
310
416
  }
311
- else if (event.key === "Home" || event.key === "End") {
417
+ else if (inBody && (event.key === "Home" || event.key === "End")) {
312
418
  event.preventDefault();
313
419
  moveFocus(event.key === "Home" ? "first" : "last");
314
420
  }
421
+ else if (inBody && (event.key === "PageDown" || event.key === "PageUp")) {
422
+ event.preventDefault();
423
+ pageFocus(event.key === "PageDown" ? 1 : -1);
424
+ }
315
425
  else if (event.key === "Backspace" && query) {
316
426
  event.preventDefault();
317
427
  setQuery(query.slice(0, -1));
318
- (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
428
+ focusField();
319
429
  }
320
430
  else if (event.key.length === 1 &&
321
431
  !event.metaKey &&
@@ -326,9 +436,46 @@ export default function NavLauncher({ isOpen, onClose, sections: rawSections, pi
326
436
  return;
327
437
  event.preventDefault();
328
438
  setQuery(query + event.key);
329
- (_b = inputRef.current) === null || _b === void 0 ? void 0 : _b.focus();
439
+ focusField();
330
440
  }
331
441
  };
442
+ const onContentKeyDown = (event) => {
443
+ if (event.key === "Escape") {
444
+ event.stopPropagation();
445
+ if (event.nativeEvent.isComposing)
446
+ return;
447
+ event.preventDefault();
448
+ if (query) {
449
+ setQuery("");
450
+ focusField();
451
+ }
452
+ else {
453
+ onClose();
454
+ }
455
+ return;
456
+ }
457
+ const hasMod = event.metaKey || event.ctrlKey;
458
+ if (hasMod &&
459
+ !event.altKey &&
460
+ !event.shiftKey &&
461
+ event.key.toLowerCase() === "k") {
462
+ event.preventDefault();
463
+ focusField(true);
464
+ return;
465
+ }
466
+ const target = event.target;
467
+ if (target === inputRef.current)
468
+ return;
469
+ if (event.key === "/" &&
470
+ !hasMod &&
471
+ !event.altKey &&
472
+ !target.closest(TEXT_TARGET)) {
473
+ event.preventDefault();
474
+ focusField(true);
475
+ return;
476
+ }
477
+ onBodyKeyDown(event);
478
+ };
332
479
  useEffect(() => {
333
480
  var _a, _b, _c;
334
481
  (_c = (_b = (_a = bodyRef.current) === null || _a === void 0 ? void 0 : _a.querySelector('[data-active="true"]')) === null || _b === void 0 ? void 0 : _b.scrollIntoView) === null || _c === void 0 ? void 0 : _c.call(_b, { block: "nearest" });
@@ -352,7 +499,6 @@ export default function NavLauncher({ isOpen, onClose, sections: rawSections, pi
352
499
  document.removeEventListener("pointerdown", forget);
353
500
  };
354
501
  }, [isOpen]);
355
- const appList = apps !== null && apps !== void 0 ? apps : NO_APPS;
356
502
  useEffect(() => {
357
503
  var _a, _b;
358
504
  if (lastItemIndex.current < 0 || document.activeElement !== document.body)
@@ -363,23 +509,20 @@ export default function NavLauncher({ isOpen, onClose, sections: rawSections, pi
363
509
  items[Math.min(lastItemIndex.current, items.length - 1)].focus();
364
510
  }, [appList]);
365
511
  const hues = useMemo(() => assignAppHues(appList, chartMode(colors)), [appList, colors]);
366
- const showApps = appList.some((app) => app.id !== currentAppId) &&
367
- (appsVisibility === "always" || !isDesktop);
368
512
  const handleAppSelect = (app, event) => {
369
513
  onAppSelect === null || onAppSelect === void 0 ? void 0 : onAppSelect(app, event);
370
514
  if (!event.defaultPrevented)
371
515
  onClose();
372
516
  };
373
- const appsBlock = showApps ? (_jsx(AppsSection, { apps: appList, hues: hues, currentAppId: currentAppId, heading: labels.apps, onSelect: handleAppSelect })) : null;
517
+ const renderApps = (hideHeading) => (_jsx(AppsSection, { apps: appList, hues: hues, currentAppId: currentAppId, heading: labels.apps, hideHeading: hideHeading, onSelect: handleAppSelect }));
374
518
  const listboxId = `${baseId}-results`;
375
519
  const optionId = (index) => `${baseId}-opt-${index}`;
376
520
  const togglePinFor = (id) => onTogglePin ? () => onTogglePin(id) : undefined;
377
521
  const hasResults = results.length > 0;
378
522
  const trimmed = query.trim();
379
- const railFrequent = frequentItems
380
- .filter(({ screen }) => !pinned.has(screen.id))
381
- .slice(0, frequentLimit);
382
- const rail = (_jsxs(Rail, { label: labels.quickAccess, children: [_jsxs(Flex, { as: "section", direction: "column", gap: "6px", children: [_jsx(SectionHeader, { label: labels.pinned, meta: `(${pinnedItems.length})`, isRail: true }), pinnedItems.length === 0 ? (_jsx(Text, { fontSize: "13px", color: colors.textMuted, px: "4px", children: labels.pinnedEmpty })) : (_jsx(Box, { "data-nav-grid": true, "data-nav-columns": PINNED_COLUMNS, display: "grid", gridTemplateColumns: `repeat(${PINNED_COLUMNS}, minmax(0, 1fr))`, gap: "6px", children: pinnedItems.map(({ screen, module }) => (_jsx(PinnedTile, { screen: screen, module: module, link: link, labels: labels, onTogglePin: togglePinFor(screen.id) }, screen.id))) }))] }), railFrequent.length ? (_jsxs(Flex, { as: "section", direction: "column", gap: "2px", children: [_jsx(SectionHeader, { label: labels.frequent, isRail: true, hasCount: false }), railFrequent.map(({ screen, module }) => (_jsx(RailRow, { screen: screen, module: module, link: link, isStrong: true, pinId: onTogglePin ? screen.id : undefined, trailing: _jsxs(_Fragment, { children: [onTogglePin ? (_jsx(PinTextButton, { label: labels.pin(screen.label), text: labels.pinShort, onToggle: () => onTogglePin(screen.id) })) : null, _jsx(NewTabLink, { href: screen.href, label: labels.openInNewTab(screen.label), linkAs: link.linkAs })] }) }, screen.id)))] })) : null, recentItems.length ? (_jsxs(Flex, { as: "section", direction: "column", gap: "2px", children: [_jsx(SectionHeader, { label: labels.recent, isRail: true, hasCount: false }), recentItems.map(({ screen, module }) => (_jsx(RailRow, { screen: screen, module: module, link: link, trailing: _jsx(NewTabLink, { href: screen.href, label: labels.openInNewTab(screen.label), linkAs: link.linkAs }) }, screen.id)))] })) : null, appsBlock] }));
523
+ const pinnedColumns = !isLg && isDesktop ? 3 : PINNED_COLUMNS;
524
+ const quickSections = (_jsxs(_Fragment, { children: [_jsxs(Flex, { as: "section", direction: "column", gap: "6px", children: [_jsx(SectionHeader, { label: labels.pinned, meta: `(${pinnedItems.length})`, isRail: true }), pinnedItems.length === 0 ? (_jsx(Text, { fontSize: "13px", color: colors.textMuted, px: "4px", children: labels.pinnedEmpty })) : (_jsx(Box, { "data-nav-grid": true, "data-nav-columns": pinnedColumns, display: "grid", gridTemplateColumns: `repeat(${pinnedColumns}, minmax(0, 1fr))`, gap: "6px", children: pinnedItems.map(({ screen, module }) => (_jsx(PinnedTile, { screen: screen, module: module, link: link, labels: labels, onTogglePin: togglePinFor(screen.id) }, screen.id))) }))] }), railFrequent.length ? (_jsxs(Flex, { as: "section", direction: "column", gap: "2px", children: [_jsx(SectionHeader, { label: labels.frequent, isRail: true, hasCount: false }), railFrequent.map(({ screen, module }) => (_jsx(RailRow, { screen: screen, module: module, link: link, isStrong: true, pinId: onTogglePin ? screen.id : undefined, trailing: _jsxs(_Fragment, { children: [onTogglePin ? (_jsx(PinTextButton, { label: labels.pin(screen.label), text: labels.pinShort, onToggle: () => onTogglePin(screen.id) })) : null, _jsx(NewTabLink, { href: screen.href, label: labels.openInNewTab(screen.label), tip: labels.newTabTip, linkAs: link.linkAs })] }) }, screen.id)))] })) : null, recentItems.length ? (_jsxs(Flex, { as: "section", direction: "column", gap: "2px", children: [_jsx(SectionHeader, { label: labels.recent, isRail: true, hasCount: false }), recentItems.map(({ screen, module }) => (_jsx(RailRow, { screen: screen, module: module, link: link, trailing: _jsx(NewTabLink, { href: screen.href, label: labels.openInNewTab(screen.label), tip: labels.newTabTip, linkAs: link.linkAs }) }, screen.id)))] })) : null] }));
525
+ const rail = (_jsxs(Rail, { label: labels.quickAccess, scrolls: isLg, children: [quickSections, showApps ? renderApps(false) : null] }));
383
526
  const browsePane = sections.length === 0 ? (_jsx(EmptyState, { title: labels.empty, titleAs: "h3", size: "sm" })) : (sections.map((section) => (_jsxs(Flex, { as: "section", "aria-label": section.label, direction: "column", gap: "3px", children: [_jsx(SectionHeader, { label: section.label, meta: `(${section.modules.length})` }), _jsx(ItemGrid, { columns: columns, children: section.modules.map((module) => {
384
527
  const pinId = module.screens[0].id;
385
528
  return (_jsx(ModuleRow, { module: module, keyScreens: module.screens
@@ -398,16 +541,23 @@ export default function NavLauncher({ isOpen, onClose, sections: rawSections, pi
398
541
  const index = groupStarts[groupIndex] + itemIndex;
399
542
  return (_jsx(SearchRow, { item: item, query: query, isActive: index === active, link: link, labels: labels, optionId: optionId(index), isPinned: item.screenId ? pinned.has(item.screenId) : false, canPin: Boolean(onTogglePin) && item.kind === "module", onPin: () => item.screenId && (onTogglePin === null || onTogglePin === void 0 ? void 0 : onTogglePin(item.screenId)), onOpen: () => open(item, false), onHover: () => setActiveIndex(index) }, item.id));
400
543
  }) })] }, group.id))) })) : (noResults);
401
- const body = isLoading ? (_jsx(SkeletonLayout, { label: labels.loading })) : (_jsxs(_Fragment, { children: [_jsx(Pane, { children: isSearchView ? searchPane : browsePane }), rail] }));
402
- const pill = resolveFilledSurface(colors.primary, colors);
403
- return (_jsxs(Drawer, { isOpen: isOpen, onClose: onClose, placement: "left", size: "full", initialFocusRef: inputRef, hideCloseButton: true, contentProps: {
404
- maxW: { base: "100vw", lg: `min(${maxWidth}, calc(100vw - 56px))` },
405
- h: { base: "100%", lg: "auto" },
406
- maxH: "100%",
407
- bg: colors.surfaceRaised,
408
- borderBottomRightRadius: { base: 0, lg: "16px" },
409
- boxShadow: colors.boxShadow.default,
410
- overflow: "hidden",
411
- ...{ "data-testid": testId },
412
- }, children: [_jsx(DrawerHeader, { p: 0, flexShrink: 0, children: _jsxs(Flex, { align: "center", gap: "12px", h: "60px", pl: "18px", pr: "12px", children: [_jsxs(Heading, { as: "h2", display: "inline-flex", alignItems: "center", gap: "7px", h: "32px", pl: "9px", pr: "11px", borderRadius: "9px", bg: pill.fill, color: pill.color, fontSize: "13px", fontWeight: 600, flexShrink: 0, children: [_jsx(Compass, { size: 16, strokeWidth: 2, "aria-hidden": true }), labels.title] }), _jsx(SearchField, { inputRef: inputRef, value: query, onChange: setQuery, onKeyDown: onInputKeyDown, labels: labels, hint: searchHint, listboxId: listboxId, activeId: isSearchView && hasResults ? optionId(active) : undefined, isExpanded: isSearchView, hasResults: hasResults }), _jsx(IconButton, { "aria-label": labels.close, icon: _jsx(X, { size: 18, "aria-hidden": true }), variant: "ghost", boxSize: "34px", minW: "34px", borderRadius: "9px", color: colors.textMuted, ml: "auto", onClick: onClose })] }) }), _jsx(DrawerBody, { p: 0, display: "flex", flexDirection: { base: "column", lg: "row" }, overflowY: { base: "auto", lg: "hidden" }, minH: 0, children: _jsx(Flex, { ref: bodyRef, onKeyDown: onBodyKeyDown, direction: { base: "column", lg: "row" }, flex: 1, minH: 0, minW: 0, children: body }) }), _jsxs(DrawerFooter, { p: 0, minH: "32px", flexShrink: 0, px: "18px", py: "6px", gap: "6px 14px", flexWrap: "wrap", bg: colors.backgroundColor.subtle, fontSize: "11.5px", color: colors.textMuted, whiteSpace: "nowrap", justifyContent: "flex-start", children: [_jsxs(Flex, { gap: "6px 14px", align: "center", wrap: "wrap", "aria-hidden": true, sx: { "@media (hover: none)": { display: "none" } }, children: [openShortcutHint ? (_jsxs(Flex, { as: "span", align: "center", gap: "5px", children: [_jsx(Kbd, { size: "xs", children: openShortcutHint }), labels.hintOpenMenu] })) : null, _jsxs(Flex, { as: "span", align: "center", gap: "5px", children: [_jsx(Flex, { as: "span", gap: "2px", children: ARROW_KEYS.map((arrow) => (_jsx(Kbd, { size: "xs", children: arrow }, arrow))) }), labels.hintMove] }), _jsxs(Flex, { as: "span", align: "center", gap: "5px", children: [_jsx(KbdShortcut, { shortcut: "enter", size: "xs" }), labels.hintOpen] }), _jsxs(Flex, { as: "span", align: "center", gap: "5px", children: [modKeyLabel ? (_jsxs(Flex, { as: "span", gap: "4px", children: [_jsx(Kbd, { size: "xs", children: modKeyLabel }), _jsx(Kbd, { size: "xs", children: ENTER_KEY })] })) : (_jsx(KbdShortcut, { shortcut: "mod+enter", size: "xs" })), labels.hintNewTab] }), onTogglePin ? (_jsxs(Flex, { as: "span", align: "center", gap: "5px", children: [_jsx(Kbd, { size: "xs", children: "P" }), labels.hintPin] })) : null, _jsxs(Flex, { as: "span", align: "center", gap: "5px", children: [_jsx(KbdShortcut, { shortcut: "esc", size: "xs" }), labels.hintClose] })] }), _jsx(Text, { as: "span", ml: "auto", children: labels.footerNote })] })] }));
544
+ const stacked = (_jsxs(_Fragment, { children: [_jsx(Pane, { scrolls: isLg, children: isSearchView ? searchPane : browsePane }), rail] }));
545
+ const body = isLoading ? _jsx(SkeletonLayout, { label: labels.loading }) : stacked;
546
+ const tabPanels = {
547
+ menu: _jsx(Pane, { children: browsePane }),
548
+ quick: (_jsx(Flex, { direction: "column", gap: "16px", px: "18px", pt: "14px", pb: "16px", children: quickSections })),
549
+ apps: (_jsx(Box, { px: "14px", pt: "10px", pb: "16px", children: renderApps(true) })),
550
+ };
551
+ const compactBody = (_jsx(Flex, { "data-scroll-region": true, role: isTabbed ? "tabpanel" : undefined, id: isTabbed ? panelId(baseId) : undefined, "aria-labelledby": isTabbed ? tabId(baseId, activeTab) : undefined, direction: "column", flex: 1, minH: 0, overflowY: "auto", children: isTabbed ? tabPanels[activeTab] : body }));
552
+ return (_jsx(TouchDensity, { value: !isLg, children: _jsxs(Drawer, { isOpen: isOpen, onClose: onClose, placement: "left", size: "full", initialFocusRef: inputRef, hideCloseButton: true, closeOnEsc: false, contentProps: {
553
+ maxW: { base: "100vw", lg: `min(${maxWidth}, calc(100vw - 56px))` },
554
+ h: { base: "100%", lg: "auto" },
555
+ maxH: "100%",
556
+ bg: colors.surfaceRaised,
557
+ borderBottomRightRadius: { base: 0, lg: "16px" },
558
+ boxShadow: colors.boxShadow.default,
559
+ overflow: "hidden",
560
+ onKeyDown: onContentKeyDown,
561
+ ...{ "data-testid": testId },
562
+ }, children: [_jsx(DrawerHeader, { p: 0, flexShrink: 0, children: _jsxs(Flex, { align: "center", gap: { base: "8px", md: "12px" }, minH: { base: "60px", md: "68px" }, pl: { base: "12px", md: "18px" }, pr: { base: "8px", md: "12px" }, children: [_jsx(VisuallyHidden, { as: "h2", children: labels.title }), _jsx(SearchField, { inputRef: inputRef, value: query, onChange: setQuery, onKeyDown: onInputKeyDown, labels: labels, hint: searchHint, listboxId: listboxId, activeId: isSearchView && hasResults ? optionId(active) : undefined, isExpanded: isSearchView, hasResults: hasResults }), _jsx(Box, { "aria-hidden": true, "data-testid": "launcher-count", display: { base: "none", sm: "block" }, flexShrink: 0, overflow: "hidden", w: countText ? "88px" : "0", opacity: countText ? 1 : 0, textAlign: "right", fontSize: "13px", whiteSpace: "nowrap", color: colors.textMuted, transition: transitionFor(["width", "opacity"]), sx: { fontVariantNumeric: "tabular-nums", ...reducedMotionSx() }, children: countText }), headerActions ? (_jsx(Flex, { align: "center", gap: "4px", flexShrink: 0, children: headerActions })) : null, _jsx(IconButton, { "aria-label": labels.close, icon: _jsx(X, { size: 18, "aria-hidden": true }), variant: "ghost", boxSize: { base: "40px", md: "44px" }, minW: { base: "40px", md: "44px" }, borderRadius: "10px", flexShrink: 0, color: colors.textMuted, onClick: onClose })] }) }), _jsx(DrawerBody, { p: 0, display: "flex", flexDirection: "column", overflowY: "hidden", minH: 0, children: _jsxs(Flex, { ref: bodyRef, direction: "column", flex: 1, minH: 0, minW: 0, children: [_jsx(VisuallyHidden, { "data-testid": "launcher-live", "aria-live": "polite", "aria-atomic": "true", children: countText }), isTabbed ? (_jsx(LauncherTabs, { tabs: tabs, selected: activeTab, onSelect: setTab, idBase: baseId, label: labels.tabsLabel, onLeaveUp: () => focusField(), onLeaveDown: () => moveFocus("first") })) : null, isLg ? (_jsx(Flex, { direction: "row", flex: 1, minH: 0, minW: 0, children: body })) : (compactBody)] }) }), _jsxs(DrawerFooter, { p: 0, minH: "32px", flexShrink: 0, px: "18px", py: "6px", gap: "6px 14px", flexWrap: "wrap", bg: colors.backgroundColor.subtle, fontSize: "11.5px", color: colors.textMuted, whiteSpace: "nowrap", justifyContent: "flex-start", children: [_jsxs(Flex, { gap: "6px 14px", align: "center", wrap: "wrap", "aria-hidden": true, sx: { "@media (hover: none)": { display: "none" } }, children: [_jsxs(Flex, { as: "span", align: "center", gap: "5px", children: [_jsx(Flex, { as: "span", gap: "2px", children: ARROW_KEYS.map((arrow) => (_jsx(Kbd, { size: "xs", children: arrow }, arrow))) }), labels.hintMove] }), _jsxs(Flex, { as: "span", align: "center", gap: "5px", children: [_jsx(KbdShortcut, { shortcut: "enter", size: "xs" }), labels.hintOpen] }), _jsxs(Flex, { as: "span", align: "center", gap: "5px", children: [modKeyLabel ? (_jsxs(Flex, { as: "span", gap: "4px", children: [_jsx(Kbd, { size: "xs", children: modKeyLabel }), _jsx(Kbd, { size: "xs", children: ENTER_KEY })] })) : (_jsx(KbdShortcut, { shortcut: "mod+enter", size: "xs" })), labels.hintNewTab] }), _jsxs(Flex, { as: "span", align: "center", gap: "5px", children: [_jsx(Kbd, { size: "xs", children: modKeyLabel !== null && modKeyLabel !== void 0 ? modKeyLabel : MOD_KEY }), labels.hintClickNewTab] }), onTogglePin ? (_jsxs(Flex, { as: "span", align: "center", gap: "5px", children: [_jsx(Kbd, { size: "xs", children: "P" }), labels.hintPin] })) : null, _jsxs(Flex, { as: "span", align: "center", gap: "5px", children: [_jsx(KbdShortcut, { shortcut: "esc", size: "xs" }), labels.hintClose] })] }), _jsx(Text, { as: "span", ml: "auto", children: labels.footerNote })] })] }) }));
413
563
  }
@@ -3,9 +3,10 @@ import { Box } from "@chakra-ui/react";
3
3
  import { ArrowUpRight } from "lucide-react";
4
4
  import { transitionFor } from "../../Theme/tokens/primitives/motion.js";
5
5
  import { useCustomTheme } from "../../Theme/useCustomTheme.js";
6
+ import ToolTip from "../ToolTip/ToolTip.js";
6
7
  import { revealStyles } from "./revealStyles.js";
7
- /** "Open in a new tab" anchor; visible on row hover, focus-within or its own focus only. */
8
- export default function NewTabLink({ href, label, linkAs = "a", tabIndex }) {
8
+ /** "Open in a new tab" anchor with a tooltip; visible on row hover, keyboard-active or focus-within, or its own focus. */
9
+ export default function NewTabLink({ href, label, tip, linkAs = "a", tabIndex, }) {
9
10
  const { colors } = useCustomTheme();
10
- return (_jsx(Box, { as: linkAs, href: href, target: "_blank", rel: "noopener noreferrer", "aria-label": label, title: label, tabIndex: tabIndex, display: "inline-flex", alignItems: "center", justifyContent: "center", w: "22px", h: "22px", flexShrink: 0, borderRadius: "md", color: colors.textMuted, opacity: 0, transition: transitionFor(["opacity", "background-color"]), sx: revealStyles(colors), _hover: { bg: colors.backgroundColor.subtle }, children: _jsx(ArrowUpRight, { size: 13, "aria-hidden": true }) }));
11
+ return (_jsx(ToolTip, { label: tip !== null && tip !== void 0 ? tip : label, placement: "bottom", children: _jsx(Box, { as: linkAs, href: href, target: "_blank", rel: "noopener noreferrer", "aria-label": label, tabIndex: tabIndex, display: "inline-flex", alignItems: "center", justifyContent: "center", w: "22px", h: "22px", flexShrink: 0, borderRadius: "md", color: colors.textMuted, opacity: 0, transition: transitionFor(["opacity", "background-color"]), sx: revealStyles(colors), _hover: { bg: colors.backgroundColor.subtle }, children: _jsx(ArrowUpRight, { size: 13, "aria-hidden": true }) }) }));
11
12
  }
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { Box, Flex, Heading, Text } from "@chakra-ui/react";
3
3
  import { Lock, Star } from "lucide-react";
4
- import { Fragment, } from "react";
4
+ import { createContext, Fragment, useContext, } from "react";
5
5
  import { FOCUS_VISIBLE_OVER_STATE, focusVisibleStyles, } from "../../Theme/chakra/focusRing.styles.js";
6
6
  import { resolveFilledSurface } from "../../Theme/tokens/builders/filledSurface.js";
7
7
  import { AA_NON_TEXT, liftToContrast, } from "../../Theme/tokens/builders/outlineRung.js";
@@ -15,6 +15,9 @@ import NewTabLink from "./NewTabLink.js";
15
15
  import PinToggle from "./PinToggle.js";
16
16
  import { revealStyles } from "./revealStyles.js";
17
17
  export const ITEM_SELECTOR = "[data-launcher-item]";
18
+ const TouchDensityContext = createContext(false);
19
+ export const TouchDensity = TouchDensityContext.Provider;
20
+ export const useTouchDensity = () => useContext(TouchDensityContext);
18
21
  export const isPlainClick = (event) => event.button === 0 &&
19
22
  !event.metaKey &&
20
23
  !event.ctrlKey &&
@@ -113,6 +116,7 @@ export function Highlight({ text, query }) {
113
116
  }
114
117
  export function RowActions({ children }) {
115
118
  const { colors } = useCustomTheme();
119
+ const isTouch = useTouchDensity();
116
120
  const revealed = REVEALED.replace(/ &/g, " > &");
117
121
  return (_jsx(Flex, { sx: {
118
122
  [NARROW_ROW]: {
@@ -121,7 +125,7 @@ export function RowActions({ children }) {
121
125
  borderRadius: "6px",
122
126
  },
123
127
  },
124
- }, position: "absolute", top: "7px", right: "4px", h: "19px", align: "center", gap: "2px", zIndex: 1, children: children }));
128
+ }, position: "absolute", top: isTouch ? "12px" : "7px", right: "4px", h: "19px", align: "center", gap: "2px", zIndex: 1, children: children }));
125
129
  }
126
130
  export function RowTip({ text }) {
127
131
  const { colors } = useCustomTheme();
@@ -132,19 +136,21 @@ export function ModuleRow({ module, keyScreens, isPinned, onTogglePin, pinId, la
132
136
  const hover = useRowHover();
133
137
  const focus = useRowFocus();
134
138
  const ring = focusVisibleStyles(colors);
139
+ const isTouch = useTouchDensity();
135
140
  const base = typeof module.badge === "number" || module.badge ? 24 : 0;
136
141
  const open = base + (onTogglePin ? 46 : 24);
137
- return (_jsxs(Flex, { "data-group": true, "data-nav-cell": true, position: "relative", align: "flex-start", gap: "10px", minH: "34px", pl: "8px", pr: "4px", pt: "7px", pb: "5px", borderRadius: "8px", transition: transitionFor(["background-color"]), _hover: hover._hover, _focusWithin: hover._focusWithin, sx: { ...focus.sx, ...ROW_CONTAINER }, children: [_jsx(RowIcon, { icon: module.icon, size: 16, mt: "2px" }), _jsxs(Flex, { direction: "column", flex: 1, minW: 0, children: [_jsxs(Flex, { align: "center", gap: "6px", h: "19px", minW: 0, sx: titleInset(base, open), children: [_jsx(OverflowToolTip, { label: module.label, placement: "top-start", children: _jsx(Box, { as: link.linkAs, href: module.href, "data-launcher-item": true, ...focus.primary, "data-pin-id": onTogglePin ? pinId : undefined, onClick: link.onPlainClick, minW: 0, fontSize: "13.5px", fontWeight: 500, color: colors.text[900], noOfLines: 1, borderRadius: "sm", sx: stretchedLink("8px"), children: module.label }) }), module.isNew ? _jsx(NewTag, { label: labels.newBadge }) : null] }), keyScreens.length ? (_jsx(OverflowToolTip, { label: keyScreens.map((screen) => screen.label).join(" · "), placement: "bottom-start", children: _jsx(Box, { role: "group", "aria-label": `${module.label} ${labels.screens}`, position: "relative", zIndex: 1, w: "fit-content", maxW: "100%", fontSize: "11.5px", lineHeight: "15px", color: colors.textMuted, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", children: keyScreens.map((screen, i) => (_jsxs(Fragment, { children: [i > 0 ? (_jsx(Box, { as: "span", "aria-hidden": true, mx: "5px", opacity: 0.7, children: "\u00B7" })) : null, _jsx(Box, { as: link.linkAs, href: screen.href, "data-launcher-item": true, onClick: link.onPlainClick, color: "inherit", borderRadius: "sm", position: "relative", zIndex: 1, _hover: {
142
+ return (_jsxs(Flex, { "data-group": true, "data-nav-cell": true, position: "relative", align: "flex-start", gap: "10px", minH: isTouch ? "44px" : "34px", pl: "8px", pr: "4px", pt: isTouch ? "12px" : "7px", pb: isTouch ? "8px" : "5px", borderRadius: "8px", transition: transitionFor(["background-color"]), _hover: hover._hover, _focusWithin: hover._focusWithin, sx: { ...focus.sx, ...ROW_CONTAINER }, children: [_jsx(RowIcon, { icon: module.icon, size: 16, mt: "2px" }), _jsxs(Flex, { direction: "column", flex: 1, minW: 0, children: [_jsxs(Flex, { align: "center", gap: "6px", h: "19px", minW: 0, sx: titleInset(base, open), children: [_jsx(OverflowToolTip, { label: module.label, placement: "top-start", children: _jsx(Box, { as: link.linkAs, href: module.href, "data-launcher-item": true, ...focus.primary, "data-pin-id": onTogglePin ? pinId : undefined, onClick: link.onPlainClick, minW: 0, fontSize: "13.5px", fontWeight: 500, color: colors.text[900], noOfLines: 1, borderRadius: "sm", sx: stretchedLink("8px"), children: module.label }) }), module.isNew ? _jsx(NewTag, { label: labels.newBadge }) : null] }), keyScreens.length ? (_jsx(OverflowToolTip, { label: keyScreens.map((screen) => screen.label).join(" · "), placement: "bottom-start", children: _jsx(Box, { role: "group", "aria-label": `${module.label} ${labels.screens}`, position: "relative", zIndex: 1, w: "fit-content", maxW: "100%", fontSize: "11.5px", lineHeight: "15px", color: colors.textMuted, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", children: keyScreens.map((screen, i) => (_jsxs(Fragment, { children: [i > 0 ? (_jsx(Box, { as: "span", "aria-hidden": true, mx: "5px", opacity: 0.7, children: "\u00B7" })) : null, _jsx(Box, { as: link.linkAs, href: screen.href, "data-launcher-item": true, onClick: link.onPlainClick, color: "inherit", borderRadius: "sm", position: "relative", zIndex: 1, _hover: {
138
143
  textDecoration: "underline",
139
144
  textUnderlineOffset: "2px",
140
- }, sx: ring, children: screen.label })] }, screen.id))) }) })) : null] }), _jsxs(RowActions, { children: [_jsx(CountBadge, { value: module.badge }), onTogglePin ? (_jsx(PinToggle, { isPinned: isPinned, label: isPinned ? labels.unpin(module.label) : labels.pin(module.label), title: "", onToggle: onTogglePin })) : null, _jsx(NewTabLink, { href: module.href, label: labels.openInNewTab(module.label), linkAs: link.linkAs })] }), onTogglePin ? (_jsx(RowTip, { text: isPinned ? labels.unpinTip : labels.pinTip })) : null] }));
145
+ }, sx: ring, children: screen.label })] }, screen.id))) }) })) : null] }), _jsxs(RowActions, { children: [_jsx(CountBadge, { value: module.badge }), onTogglePin ? (_jsx(PinToggle, { isPinned: isPinned, label: isPinned ? labels.unpin(module.label) : labels.pin(module.label), title: "", onToggle: onTogglePin })) : null, _jsx(NewTabLink, { href: module.href, label: labels.openInNewTab(module.label), tip: labels.newTabTip, linkAs: link.linkAs })] }), onTogglePin ? (_jsx(RowTip, { text: isPinned ? labels.unpinTip : labels.pinTip })) : null] }));
141
146
  }
142
147
  export function RailRow({ screen, module, link, isStrong = false, trailing, pinId, }) {
143
148
  var _a;
144
149
  const { colors } = useCustomTheme();
145
150
  const hover = useRowHover();
146
151
  const focus = useRowFocus();
147
- return (_jsxs(Flex, { "data-group": true, position: "relative", align: "center", gap: "10px", h: "32px", pl: "8px", pr: "4px", borderRadius: "8px", transition: transitionFor(["background-color"]), _hover: hover._hover, _focusWithin: hover._focusWithin, sx: focus.sx, children: [_jsx(RowIcon, { icon: (_a = screen.icon) !== null && _a !== void 0 ? _a : module.icon, size: 15 }), _jsx(OverflowToolTip, { label: screen.label, placement: "top-start", children: _jsx(Box, { as: link.linkAs, href: screen.href, "data-launcher-item": true, ...focus.primary, "data-pin-id": pinId, onClick: link.onPlainClick, flex: 1, minW: 0, fontSize: "13px", fontWeight: isStrong ? 500 : 400, color: colors.text[900], noOfLines: 1, borderRadius: "sm", sx: stretchedLink("8px"), children: screen.label }) }), _jsx(Flex, { position: "relative", zIndex: 1, align: "center", gap: "2px", children: trailing })] }));
152
+ const isTouch = useTouchDensity();
153
+ return (_jsxs(Flex, { "data-group": true, position: "relative", align: "center", gap: "10px", h: isTouch ? "44px" : "32px", pl: "8px", pr: "4px", borderRadius: "8px", transition: transitionFor(["background-color"]), _hover: hover._hover, _focusWithin: hover._focusWithin, sx: focus.sx, children: [_jsx(RowIcon, { icon: (_a = screen.icon) !== null && _a !== void 0 ? _a : module.icon, size: 15 }), _jsx(OverflowToolTip, { label: screen.label, placement: "top-start", children: _jsx(Box, { as: link.linkAs, href: screen.href, "data-launcher-item": true, ...focus.primary, "data-pin-id": pinId, onClick: link.onPlainClick, flex: 1, minW: 0, fontSize: "13px", fontWeight: isStrong ? 500 : 400, color: colors.text[900], noOfLines: 1, borderRadius: "sm", sx: stretchedLink("8px"), children: screen.label }) }), _jsx(Flex, { position: "relative", zIndex: 1, align: "center", gap: "2px", children: trailing })] }));
148
154
  }
149
155
  export function PinTextButton({ label, text, onToggle }) {
150
156
  const { colors } = useCustomTheme();
@@ -159,13 +165,14 @@ export function PinnedTile({ screen, module, link, labels, onTogglePin, }) {
159
165
  const { colors } = useCustomTheme();
160
166
  const Icon = (_a = screen.icon) !== null && _a !== void 0 ? _a : module.icon;
161
167
  const focus = useRowFocus(true);
162
- return (_jsxs(Flex, { "data-group": true, "data-nav-cell": true, position: "relative", align: "center", gap: "8px", h: "36px", pl: "9px", pr: "4px", borderRadius: "9px", borderWidth: "1px", borderColor: colors.boxborder[200], bg: colors.surfaceRaised, transition: transitionFor(["border-color", "background-color"]), _hover: { borderColor: colors.gray[300] }, sx: focus.sx, children: [Icon ? (_jsx(Box, { as: "span", display: "inline-flex", flexShrink: 0, color: colors.accentText, children: _jsx(Icon, { size: 15, strokeWidth: 1.8, "aria-hidden": true }) })) : null, _jsx(OverflowToolTip, { label: screen.label, placement: "top-start", children: _jsx(Box, { as: link.linkAs, href: screen.href, "data-launcher-item": true, ...focus.primary, "data-pin-id": onTogglePin ? screen.id : undefined, onClick: link.onPlainClick, flex: 1, minW: 0, fontSize: "13px", fontWeight: 500, color: colors.text[900], noOfLines: 1, borderRadius: "sm", sx: stretchedLink("9px"), children: screen.label }) }), _jsx(CountBadge, { value: screen.badge, size: "tile" }), onTogglePin ? (_jsx(Box, { position: "relative", zIndex: 1, flexShrink: 0, w: "0", overflow: "hidden", transition: transitionFor(["width"]), _groupHover: { w: "22px" }, _groupFocusWithin: { w: "22px" }, children: _jsx(PinToggle, { isPinned: true, size: 12, label: labels.unpin(screen.label), title: labels.unpinTip, onToggle: onTogglePin }) })) : null] }));
168
+ const isTouch = useTouchDensity();
169
+ return (_jsxs(Flex, { "data-group": true, "data-nav-cell": true, position: "relative", align: "center", gap: "8px", h: isTouch ? "44px" : "36px", pl: "9px", pr: "4px", borderRadius: "9px", borderWidth: "1px", borderColor: colors.boxborder[200], bg: colors.surfaceRaised, transition: transitionFor(["border-color", "background-color"]), _hover: { borderColor: colors.gray[300] }, sx: focus.sx, children: [Icon ? (_jsx(Box, { as: "span", display: "inline-flex", flexShrink: 0, color: colors.accentText, children: _jsx(Icon, { size: 15, strokeWidth: 1.8, "aria-hidden": true }) })) : null, _jsx(OverflowToolTip, { label: screen.label, placement: "top-start", children: _jsx(Box, { as: link.linkAs, href: screen.href, "data-launcher-item": true, ...focus.primary, "data-pin-id": onTogglePin ? screen.id : undefined, onClick: link.onPlainClick, flex: 1, minW: 0, fontSize: "13px", fontWeight: 500, color: colors.text[900], noOfLines: 1, borderRadius: "sm", sx: stretchedLink("9px"), children: screen.label }) }), _jsx(CountBadge, { value: screen.badge, size: "tile" }), onTogglePin ? (_jsx(Box, { position: "relative", zIndex: 1, flexShrink: 0, w: "0", overflow: "hidden", transition: transitionFor(["width"]), _groupHover: { w: "22px" }, _groupFocusWithin: { w: "22px" }, children: _jsx(PinToggle, { isPinned: true, size: 12, label: labels.unpin(screen.label), title: labels.unpinTip, onToggle: onTogglePin }) })) : null] }));
163
170
  }
164
- export function AppsSection({ apps, hues, currentAppId, heading, onSelect, }) {
171
+ export function AppsSection({ apps, hues, currentAppId, heading, hideHeading = false, onSelect, }) {
165
172
  const { colors } = useCustomTheme();
166
173
  const rows = listRowBackgrounds(colors);
167
174
  const rowRing = activeRowSurface(colors);
168
- return (_jsxs(Box, { children: [_jsx(SectionHeader, { label: heading, isRail: true, hasCount: false }), _jsx(Box, { as: "ul", listStyleType: "none", m: 0, p: 0, "aria-label": heading, children: apps.map((app, index) => {
175
+ return (_jsxs(Box, { children: [hideHeading ? null : _jsx(SectionHeader, { label: heading, isRail: true, hasCount: false }), _jsx(Box, { as: "ul", listStyleType: "none", m: 0, p: 0, "aria-label": heading, children: apps.map((app, index) => {
169
176
  var _a;
170
177
  const isCurrent = app.id === currentAppId;
171
178
  const interactive = !isCurrent && !app.locked;