@harborclient/sdk 1.0.67 → 1.0.69

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 (57) hide show
  1. package/dist/components/FooterIcon/index.d.ts +2 -1
  2. package/dist/components/FooterIcon/index.d.ts.map +1 -1
  3. package/dist/components/FooterIcon/index.js +3 -2
  4. package/dist/components/RowActionsMenu/Submenu.d.ts +49 -0
  5. package/dist/components/RowActionsMenu/Submenu.d.ts.map +1 -0
  6. package/dist/components/RowActionsMenu/Submenu.js +156 -0
  7. package/dist/components/RowActionsMenu/index.d.ts +58 -5
  8. package/dist/components/RowActionsMenu/index.d.ts.map +1 -1
  9. package/dist/components/RowActionsMenu/index.js +293 -55
  10. package/dist/components/TabBar/ClosingTabShell.d.ts +20 -0
  11. package/dist/components/TabBar/ClosingTabShell.d.ts.map +1 -0
  12. package/dist/components/TabBar/ClosingTabShell.js +126 -0
  13. package/dist/components/TabBar/TabBarShell.d.ts +58 -0
  14. package/dist/components/TabBar/TabBarShell.d.ts.map +1 -0
  15. package/dist/components/TabBar/TabBarShell.js +37 -0
  16. package/dist/components/TabBar/TabContextMenu.d.ts +23 -0
  17. package/dist/components/TabBar/TabContextMenu.d.ts.map +1 -0
  18. package/dist/components/TabBar/TabContextMenu.js +119 -0
  19. package/dist/components/TabBar/TabNewButton.d.ts +26 -0
  20. package/dist/components/TabBar/TabNewButton.d.ts.map +1 -0
  21. package/dist/components/TabBar/TabNewButton.js +14 -0
  22. package/dist/components/TabBar/index.d.ts +92 -0
  23. package/dist/components/TabBar/index.d.ts.map +1 -0
  24. package/dist/components/TabBar/index.js +224 -0
  25. package/dist/components/TabBar/tabCloseMenuHelpers.d.ts +41 -0
  26. package/dist/components/TabBar/tabCloseMenuHelpers.d.ts.map +1 -0
  27. package/dist/components/TabBar/tabCloseMenuHelpers.js +48 -0
  28. package/dist/components/TabBar/types.d.ts +60 -0
  29. package/dist/components/TabBar/types.d.ts.map +1 -0
  30. package/dist/components/TabBar/types.js +1 -0
  31. package/dist/components/TabBar/useExitingTabItems.d.ts +73 -0
  32. package/dist/components/TabBar/useExitingTabItems.d.ts.map +1 -0
  33. package/dist/components/TabBar/useExitingTabItems.js +103 -0
  34. package/dist/components/TabBar/useSortableTabItem.d.ts +31 -0
  35. package/dist/components/TabBar/useSortableTabItem.d.ts.map +1 -0
  36. package/dist/components/TabBar/useSortableTabItem.js +29 -0
  37. package/dist/components/Toolbar/index.d.ts.map +1 -1
  38. package/dist/components/Toolbar/index.js +21 -5
  39. package/dist/components/classes.d.ts +9 -0
  40. package/dist/components/classes.d.ts.map +1 -1
  41. package/dist/components/classes.js +15 -2
  42. package/dist/components/footerBarUtils.d.ts +11 -0
  43. package/dist/components/footerBarUtils.d.ts.map +1 -0
  44. package/dist/components/footerBarUtils.js +10 -0
  45. package/dist/components/index.d.ts +3 -1
  46. package/dist/components/index.d.ts.map +1 -1
  47. package/dist/components/index.js +3 -1
  48. package/dist/components/menuPosition.d.ts +48 -0
  49. package/dist/components/menuPosition.d.ts.map +1 -0
  50. package/dist/components/menuPosition.js +49 -0
  51. package/dist/components/rowActionsMenuHelpers.d.ts +17 -0
  52. package/dist/components/rowActionsMenuHelpers.d.ts.map +1 -1
  53. package/dist/components/rowActionsMenuHelpers.js +47 -0
  54. package/dist/styles.css +12 -0
  55. package/dist/types.d.ts +2 -1
  56. package/dist/types.d.ts.map +1 -1
  57. package/package.json +7 -1
@@ -0,0 +1,37 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "@harborclient/sdk/jsx-runtime";
2
+ import { TabCloseButton } from '../TabCloseButton/index.js';
3
+ import { tabItem } from '../classes.js';
4
+ import { cn } from '../utils.js';
5
+ import { useSortableTabItem } from './useSortableTabItem.js';
6
+ /**
7
+ * Single document-style tab row with close button and optional drag reordering.
8
+ */
9
+ export function TabBarShell({ item, tabIndex, sortableId, sortableDisabled = false, exiting = false, tabIdPrefix, panelIdPrefix, maxTabWidthClass = 'max-w-[220px]', sortableCursor = 'pointer', onSelect, onClose, onContextMenu }) {
10
+ /**
11
+ * Activates this tab when the user presses Enter or Space on the tab control.
12
+ *
13
+ * @param event - Keyboard event from the tab element.
14
+ */
15
+ const handleTabKeyDown = (event) => {
16
+ if (event.key === 'Enter' || event.key === ' ') {
17
+ event.preventDefault();
18
+ onSelect(item.id);
19
+ }
20
+ };
21
+ const { setNodeRef, listeners, style } = useSortableTabItem(sortableId, sortableDisabled || exiting);
22
+ const showActive = exiting ? false : item.active || Boolean(item.highlighted);
23
+ const cursorClass = exiting || sortableDisabled
24
+ ? 'cursor-pointer'
25
+ : sortableCursor === 'grab'
26
+ ? 'cursor-grab active:cursor-grabbing'
27
+ : 'cursor-pointer';
28
+ return (_jsxs("div", { ref: exiting ? undefined : setNodeRef, style: exiting ? undefined : style, role: "tab", id: exiting ? undefined : `${tabIdPrefix}${String(item.id)}`, "aria-controls": exiting ? undefined : `${panelIdPrefix}${String(item.id)}`, "aria-selected": item.active, "aria-label": item.accessibleName, title: item.title, tabIndex: exiting ? -1 : tabIndex, className: cn('group -mb-1 flex min-h-12 shrink-0 items-stretch gap-2.5 self-stretch rounded-t-lg border border-b-2 px-4', maxTabWidthClass, exiting ? 'pointer-events-none' : cursorClass, tabItem(showActive)), onClick: exiting ? undefined : () => onSelect(item.id), onContextMenu: exiting
29
+ ? undefined
30
+ : (event) => {
31
+ event.preventDefault();
32
+ onContextMenu?.(item.id, event);
33
+ }, onKeyDown: exiting ? undefined : handleTabKeyDown, ...(sortableDisabled || exiting ? {} : listeners), children: [_jsx("span", { className: "app-no-drag flex min-w-0 flex-1 items-center gap-1.5 py-2 text-inherit", children: item.content }), !exiting && (_jsx("span", { className: "app-no-drag flex shrink-0 items-center self-center", onPointerDown: (event) => event.stopPropagation(), children: _jsx(TabCloseButton, { ariaLabel: item.closeAccessibleName, title: item.closeAccessibleName, tabIndex: 0, onClick: (event) => {
34
+ event.stopPropagation();
35
+ onClose(item.id);
36
+ } }) }))] }));
37
+ }
@@ -0,0 +1,23 @@
1
+ import type { JSX } from 'react';
2
+ import type { MenuItem } from '../RowActionsMenu/index.js';
3
+ import { type MenuPosition } from '../menuPosition.js';
4
+ interface Props {
5
+ /**
6
+ * Grouped menu entries. Each inner array is one visual group separated by a divider.
7
+ */
8
+ groups: MenuItem[][];
9
+ /**
10
+ * Cursor position used to anchor the menu panel.
11
+ */
12
+ position: MenuPosition;
13
+ /**
14
+ * Called when the menu should close without selecting an item.
15
+ */
16
+ onClose: () => void;
17
+ }
18
+ /**
19
+ * Cursor-positioned context menu for tab bars, rendered in a portal at the click point.
20
+ */
21
+ export declare function TabContextMenu({ groups, position, onClose }: Props): JSX.Element | null;
22
+ export {};
23
+ //# sourceMappingURL=TabContextMenu.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TabContextMenu.d.ts","sourceRoot":"","sources":["../../../src/components/TabBar/TabContextMenu.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,GAAG,EAAiB,MAAM,OAAO,CAAC;AAChD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,EAAE,KAAK,YAAY,EAAqB,MAAM,oBAAoB,CAAC;AAI1E,UAAU,KAAK;IACb;;OAEG;IACH,MAAM,EAAE,QAAQ,EAAE,EAAE,CAAC;IAErB;;OAEG;IACH,QAAQ,EAAE,YAAY,CAAC;IAEvB;;OAEG;IACH,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAgBD;;GAEG;AACH,wBAAgB,cAAc,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,KAAK,GAAG,GAAG,CAAC,OAAO,GAAG,IAAI,CAkJvF"}
@@ -0,0 +1,119 @@
1
+ import { jsx as _jsx } from "@harborclient/sdk/jsx-runtime";
2
+ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from '@harborclient/sdk/react';
3
+ import { clampMenuPosition } from '../menuPosition.js';
4
+ import { portalToBody } from '../portalToBody.js';
5
+ import { cn, resolveTabListKeyAction } from '../utils.js';
6
+ /**
7
+ * Tailwind classes for a single menu item button.
8
+ *
9
+ * @param variant - Visual variant for default or destructive actions.
10
+ */
11
+ function menuItemClass(variant) {
12
+ const base = 'block w-full cursor-pointer border-none bg-transparent px-3.5 py-1.5 text-left text-[16px] app-no-drag';
13
+ return variant === 'danger'
14
+ ? `${base} text-text hover:bg-danger/15 hover:text-danger`
15
+ : `${base} text-text hover:bg-selection`;
16
+ }
17
+ /**
18
+ * Cursor-positioned context menu for tab bars, rendered in a portal at the click point.
19
+ */
20
+ export function TabContextMenu({ groups, position, onClose }) {
21
+ const menuRef = useRef(null);
22
+ const itemRefs = useRef([]);
23
+ const [focusedIndex, setFocusedIndex] = useState(0);
24
+ const [clampedPosition, setClampedPosition] = useState(position);
25
+ const flatItems = useMemo(() => groups.flat(), [groups]);
26
+ /**
27
+ * Closes the menu and notifies the parent.
28
+ */
29
+ const closeMenu = useCallback(() => {
30
+ onClose();
31
+ }, [onClose]);
32
+ /**
33
+ * Focuses a menu item by index and updates roving tabindex state.
34
+ *
35
+ * @param index - Flat index into `flatItems`.
36
+ */
37
+ const focusItem = useCallback((index) => {
38
+ setFocusedIndex(index);
39
+ requestAnimationFrame(() => {
40
+ itemRefs.current[index]?.focus();
41
+ });
42
+ }, []);
43
+ /**
44
+ * Re-clamps the menu after mount and focuses the first item once dimensions are known.
45
+ */
46
+ useLayoutEffect(() => {
47
+ const menu = menuRef.current;
48
+ if (!menu) {
49
+ return;
50
+ }
51
+ const rect = menu.getBoundingClientRect();
52
+ setClampedPosition(clampMenuPosition(position, {
53
+ width: rect.width,
54
+ height: rect.height
55
+ }));
56
+ itemRefs.current[0]?.focus();
57
+ }, [position, groups]);
58
+ /**
59
+ * Closes the menu on outside click or Escape while it is open.
60
+ */
61
+ useEffect(() => {
62
+ const handleMouseDown = (event) => {
63
+ if (menuRef.current && !menuRef.current.contains(event.target)) {
64
+ closeMenu();
65
+ }
66
+ };
67
+ const handleKeyDown = (event) => {
68
+ if (event.key === 'Escape') {
69
+ event.preventDefault();
70
+ closeMenu();
71
+ }
72
+ };
73
+ document.addEventListener('mousedown', handleMouseDown);
74
+ document.addEventListener('keydown', handleKeyDown);
75
+ return () => {
76
+ document.removeEventListener('mousedown', handleMouseDown);
77
+ document.removeEventListener('keydown', handleKeyDown);
78
+ };
79
+ }, [closeMenu]);
80
+ /**
81
+ * Handles keyboard navigation within the open menu.
82
+ *
83
+ * @param event - Keyboard event from the menu panel.
84
+ */
85
+ const handleMenuKeyDown = (event) => {
86
+ if (flatItems.length === 0) {
87
+ return;
88
+ }
89
+ if (event.key === 'Tab') {
90
+ closeMenu();
91
+ return;
92
+ }
93
+ const arrowIndex = resolveTabListKeyAction(event.key, focusedIndex, flatItems.length);
94
+ if (arrowIndex !== null) {
95
+ event.preventDefault();
96
+ focusItem(arrowIndex);
97
+ }
98
+ };
99
+ if (flatItems.length === 0) {
100
+ return null;
101
+ }
102
+ return portalToBody(_jsx("div", { ref: menuRef, role: "menu", "aria-label": "Tab actions", className: "hc-tab-context-menu app-no-drag fixed z-50 min-w-[200px] rounded-md border border-separator bg-surface py-1 shadow-md", style: { left: clampedPosition.x, top: clampedPosition.y }, onKeyDown: handleMenuKeyDown, children: groups.map((group, groupIndex) => {
103
+ let flatIndex = groups
104
+ .slice(0, groupIndex)
105
+ .reduce((count, groupItems) => count + groupItems.length, 0);
106
+ return (_jsx("div", { className: groupIndex > 0
107
+ ? 'hc-tab-context-menu-group border-t border-separator'
108
+ : 'hc-tab-context-menu-group', children: group.map((item) => {
109
+ const itemIndex = flatIndex++;
110
+ return (_jsx("button", { ref: (element) => {
111
+ itemRefs.current[itemIndex] = element;
112
+ }, type: "button", role: "menuitem", tabIndex: itemIndex === focusedIndex ? 0 : -1, className: cn('hc-tab-context-menu-item', menuItemClass(item.variant)), onClick: (event) => {
113
+ event.stopPropagation();
114
+ closeMenu();
115
+ item.onSelect?.();
116
+ }, children: item.label }, item.label));
117
+ }) }, groupIndex));
118
+ }) }));
119
+ }
@@ -0,0 +1,26 @@
1
+ import type { JSX } from 'react';
2
+ interface Props {
3
+ /**
4
+ * Accessible name for the new-tab control.
5
+ */
6
+ ariaLabel: string;
7
+ /**
8
+ * Native tooltip text for the new-tab control.
9
+ */
10
+ title: string;
11
+ /**
12
+ * When true, renders a tab-height slot for wrapped tab lists; otherwise uses
13
+ * the compact circular control for horizontal scroll mode.
14
+ */
15
+ wrapped: boolean;
16
+ /**
17
+ * Called when the user opens a new tab or chat.
18
+ */
19
+ onClick: () => void;
20
+ }
21
+ /**
22
+ * New-tab "+" control shared by request editor and AI chat tab bars.
23
+ */
24
+ export declare function TabNewButton({ ariaLabel, title, wrapped, onClick }: Props): JSX.Element;
25
+ export {};
26
+ //# sourceMappingURL=TabNewButton.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TabNewButton.d.ts","sourceRoot":"","sources":["../../../src/components/TabBar/TabNewButton.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC;AAKjC,UAAU,KAAK;IACb;;OAEG;IACH,SAAS,EAAE,MAAM,CAAC;IAElB;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;;OAGG;IACH,OAAO,EAAE,OAAO,CAAC;IAEjB;;OAEG;IACH,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,KAAK,GAAG,GAAG,CAAC,OAAO,CAiCvF"}
@@ -0,0 +1,14 @@
1
+ import { jsx as _jsx } from "@harborclient/sdk/jsx-runtime";
2
+ import { faPlus } from '@fortawesome/free-solid-svg-icons';
3
+ import { FaIcon } from '../FaIcon/index.js';
4
+ import { tabItem } from '../classes.js';
5
+ import { cn } from '../utils.js';
6
+ /**
7
+ * New-tab "+" control shared by request editor and AI chat tab bars.
8
+ */
9
+ export function TabNewButton({ ariaLabel, title, wrapped, onClick }) {
10
+ if (wrapped) {
11
+ return (_jsx("div", { className: "flex shrink-0", children: _jsx("button", { type: "button", className: cn('app-no-drag -mb-1 inline-flex min-h-12 shrink-0 cursor-pointer items-center justify-center self-end rounded-t-lg border border-b-2 px-3 text-[14px]', tabItem(false)), title: title, "aria-label": ariaLabel, onClick: onClick, children: _jsx(FaIcon, { icon: faPlus, className: "h-3.5 w-3.5" }) }) }));
12
+ }
13
+ return (_jsx("div", { className: "-mb-1 flex shrink-0 items-end px-1", children: _jsx("button", { type: "button", className: "hc-tab-new-button app-no-drag mb-2.5 inline-flex shrink-0 cursor-pointer items-center justify-center border-none bg-transparent text-[14px] text-muted hover:bg-selection hover:text-text focus-visible:bg-selection focus-visible:text-text", title: title, "aria-label": ariaLabel, onClick: onClick, children: _jsx(FaIcon, { icon: faPlus, className: "h-3.5 w-3.5" }) }) }));
14
+ }
@@ -0,0 +1,92 @@
1
+ import type { JSX, ReactNode } from 'react';
2
+ import type { MenuItem } from '../RowActionsMenu/index.js';
3
+ import type { TabBarItem, TabBarNewTab, TabBarSortableCursor } from './types.js';
4
+ interface Props<TId extends string | number> {
5
+ /**
6
+ * Open tabs to render in display order.
7
+ */
8
+ tabs: TabBarItem<TId>[];
9
+ /**
10
+ * ID of the currently active tab.
11
+ */
12
+ activeId: TId;
13
+ /**
14
+ * When true, tabs wrap to multiple rows; otherwise they scroll horizontally.
15
+ */
16
+ wrap: boolean;
17
+ /**
18
+ * Accessible name for the tab list.
19
+ */
20
+ ariaLabel: string;
21
+ /**
22
+ * Prefix for tab element ids (for example `request-tab-`).
23
+ */
24
+ tabIdPrefix: string;
25
+ /**
26
+ * Prefix for linked tab panel ids (for example `request-tabpanel-`).
27
+ */
28
+ panelIdPrefix: string;
29
+ /**
30
+ * Prefix for dnd-kit sortable ids. Defaults to `${tabIdPrefix}sort:`.
31
+ */
32
+ sortablePrefix?: string;
33
+ /**
34
+ * Configuration for the new-tab control.
35
+ */
36
+ newTab: TabBarNewTab;
37
+ /**
38
+ * Called when the user selects a tab.
39
+ */
40
+ onSelect: (id: TId) => void;
41
+ /**
42
+ * Called when the user closes a tab.
43
+ */
44
+ onClose: (id: TId) => void;
45
+ /**
46
+ * Persists a new tab order after drag-and-drop reordering.
47
+ */
48
+ onReorder: (orderedIds: TId[]) => void;
49
+ /**
50
+ * Builds grouped context menu entries when a tab is right-clicked.
51
+ */
52
+ buildContextMenuGroups?: (targetId: TId, orderedIds: TId[]) => MenuItem[][];
53
+ /**
54
+ * When ArrowDown is pressed on a focused tab, returns true if focus moved into
55
+ * the linked panel (for example request editor content).
56
+ */
57
+ onArrowDownIntoPanel?: (id: TId) => boolean;
58
+ /**
59
+ * Focuses the tab control for keyboard navigation and focus restoration.
60
+ */
61
+ onFocusTab?: (id: TId) => void;
62
+ /**
63
+ * Wraps the tab row for horizontal scrolling. Defaults to overflow-x-auto.
64
+ */
65
+ renderScrollContainer?: (row: ReactNode) => ReactNode;
66
+ /**
67
+ * Maximum width class applied to each tab shell.
68
+ */
69
+ maxTabWidthClass?: string;
70
+ /**
71
+ * Cursor style when sortable drag is enabled.
72
+ */
73
+ sortableCursor?: TabBarSortableCursor;
74
+ /**
75
+ * Additional classes for the outer tab bar container.
76
+ */
77
+ className?: string;
78
+ }
79
+ /**
80
+ * Document-style tab bar with drag reorder, keyboard navigation, close animation,
81
+ * optional context menu, and a new-tab control.
82
+ */
83
+ export declare function TabBar<TId extends string | number>({ tabs, activeId, wrap, ariaLabel, tabIdPrefix, panelIdPrefix, sortablePrefix, newTab, onSelect, onClose, onReorder, buildContextMenuGroups, onArrowDownIntoPanel, onFocusTab, renderScrollContainer, maxTabWidthClass, sortableCursor, className }: Props<TId>): JSX.Element;
84
+ export type { TabBarItem, TabBarNewTab, TabBarSortableCursor } from './types.js';
85
+ export { TabNewButton } from './TabNewButton.js';
86
+ export { TabContextMenu } from './TabContextMenu.js';
87
+ export { ClosingTabShell } from './ClosingTabShell.js';
88
+ export { TabBarShell } from './TabBarShell.js';
89
+ export { useSortableTabItem } from './useSortableTabItem.js';
90
+ export { useExitingTabItems, detectRemovedTabItems, resolveInsertBeforeId, type ExitingTabItem } from './useExitingTabItems.js';
91
+ export { buildTabCloseMenuGroups, tabIdsToCloseOthers, tabIdsToCloseToTheRight } from './tabCloseMenuHelpers.js';
92
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/TabBar/index.tsx"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAkB,GAAG,EAA6B,SAAS,EAAE,MAAM,OAAO,CAAC;AACvF,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,4BAA4B,CAAC;AAM3D,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAgCjF,UAAU,KAAK,CAAC,GAAG,SAAS,MAAM,GAAG,MAAM;IACzC;;OAEG;IACH,IAAI,EAAE,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;IAExB;;OAEG;IACH,QAAQ,EAAE,GAAG,CAAC;IAEd;;OAEG;IACH,IAAI,EAAE,OAAO,CAAC;IAEd;;OAEG;IACH,SAAS,EAAE,MAAM,CAAC;IAElB;;OAEG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;OAEG;IACH,aAAa,EAAE,MAAM,CAAC;IAEtB;;OAEG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;OAEG;IACH,MAAM,EAAE,YAAY,CAAC;IAErB;;OAEG;IACH,QAAQ,EAAE,CAAC,EAAE,EAAE,GAAG,KAAK,IAAI,CAAC;IAE5B;;OAEG;IACH,OAAO,EAAE,CAAC,EAAE,EAAE,GAAG,KAAK,IAAI,CAAC;IAE3B;;OAEG;IACH,SAAS,EAAE,CAAC,UAAU,EAAE,GAAG,EAAE,KAAK,IAAI,CAAC;IAEvC;;OAEG;IACH,sBAAsB,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE,UAAU,EAAE,GAAG,EAAE,KAAK,QAAQ,EAAE,EAAE,CAAC;IAE5E;;;OAGG;IACH,oBAAoB,CAAC,EAAE,CAAC,EAAE,EAAE,GAAG,KAAK,OAAO,CAAC;IAE5C;;OAEG;IACH,UAAU,CAAC,EAAE,CAAC,EAAE,EAAE,GAAG,KAAK,IAAI,CAAC;IAE/B;;OAEG;IACH,qBAAqB,CAAC,EAAE,CAAC,GAAG,EAAE,SAAS,KAAK,SAAS,CAAC;IAEtD;;OAEG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAE1B;;OAEG;IACH,cAAc,CAAC,EAAE,oBAAoB,CAAC;IAEtC;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAqDD;;;GAGG;AACH,wBAAgB,MAAM,CAAC,GAAG,SAAS,MAAM,GAAG,MAAM,EAAE,EAClD,IAAI,EACJ,QAAQ,EACR,IAAI,EACJ,SAAS,EACT,WAAW,EACX,aAAa,EACb,cAAc,EACd,MAAM,EACN,QAAQ,EACR,OAAO,EACP,SAAS,EACT,sBAAsB,EACtB,oBAAoB,EACpB,UAAU,EACV,qBAAqB,EACrB,gBAAgB,EAChB,cAAc,EACd,SAAS,EACV,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,OAAO,CAqT1B;AAED,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AACjF,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,EACL,kBAAkB,EAClB,qBAAqB,EACrB,qBAAqB,EACrB,KAAK,cAAc,EACpB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,uBAAuB,EACvB,mBAAmB,EACnB,uBAAuB,EACxB,MAAM,0BAA0B,CAAC"}
@@ -0,0 +1,224 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "@harborclient/sdk/jsx-runtime";
2
+ import { DndContext as DndContextBase, DragOverlay as DragOverlayBase, KeyboardSensor, PointerSensor, closestCenter, useSensor, useSensors } from '@dnd-kit/core';
3
+ import { SortableContext, arrayMove, horizontalListSortingStrategy, rectSortingStrategy, sortableKeyboardCoordinates } from '@dnd-kit/sortable';
4
+ import { useEffect, useMemo, useState } from '@harborclient/sdk/react';
5
+ import { cn, resolveTabListKeyAction } from '../utils.js';
6
+ import { ClosingTabShell } from './ClosingTabShell.js';
7
+ import { TabBarShell } from './TabBarShell.js';
8
+ import { TabContextMenu } from './TabContextMenu.js';
9
+ import { TabNewButton } from './TabNewButton.js';
10
+ import { useExitingTabItems } from './useExitingTabItems.js';
11
+ const DndContext = DndContextBase;
12
+ const DragOverlay = DragOverlayBase;
13
+ /**
14
+ * Builds a stable dnd-kit sortable id for a tab row.
15
+ *
16
+ * @param prefix - Sortable id prefix.
17
+ * @param id - Open tab id.
18
+ */
19
+ function tabSortableId(prefix, id) {
20
+ return `${prefix}${String(id)}`;
21
+ }
22
+ /**
23
+ * Parses a sortable drag id back to its tab id string.
24
+ *
25
+ * @param prefix - Sortable id prefix.
26
+ * @param dragId - Sortable id from dnd-kit.
27
+ */
28
+ function parseTabSortableId(prefix, dragId) {
29
+ if (!dragId.startsWith(prefix)) {
30
+ return null;
31
+ }
32
+ return dragId.slice(prefix.length);
33
+ }
34
+ /**
35
+ * Resolves the tab list index for arrow-key navigation from keyboard focus.
36
+ *
37
+ * @param tabs - Open tabs in display order.
38
+ * @param activeId - Currently selected tab id.
39
+ * @param tabIdPrefix - Prefix for tab element ids.
40
+ * @returns Index into `tabs`, or `-1` when the list is empty.
41
+ */
42
+ function resolveFocusedTabIndex(tabs, activeId, tabIdPrefix) {
43
+ const activeElement = document.activeElement;
44
+ if (activeElement instanceof HTMLElement) {
45
+ const tabElement = activeElement.closest('[role="tab"]');
46
+ if (tabElement instanceof HTMLElement && tabElement.id.startsWith(tabIdPrefix)) {
47
+ const tabId = tabElement.id.slice(tabIdPrefix.length);
48
+ const focusedIndex = tabs.findIndex((tab) => String(tab.id) === tabId);
49
+ if (focusedIndex >= 0) {
50
+ return focusedIndex;
51
+ }
52
+ }
53
+ }
54
+ return tabs.findIndex((tab) => tab.id === activeId);
55
+ }
56
+ /**
57
+ * Document-style tab bar with drag reorder, keyboard navigation, close animation,
58
+ * optional context menu, and a new-tab control.
59
+ */
60
+ export function TabBar({ tabs, activeId, wrap, ariaLabel, tabIdPrefix, panelIdPrefix, sortablePrefix, newTab, onSelect, onClose, onReorder, buildContextMenuGroups, onArrowDownIntoPanel, onFocusTab, renderScrollContainer, maxTabWidthClass, sortableCursor, className }) {
61
+ const resolvedSortablePrefix = sortablePrefix ?? `${tabIdPrefix}sort:`;
62
+ const [activeDragId, setActiveDragId] = useState(null);
63
+ const [contextMenu, setContextMenu] = useState(null);
64
+ const getId = useMemo(() => (item) => item.id, []);
65
+ const sortableEnabled = tabs.length >= 2;
66
+ const { completeExit, getExitingBefore, removedIds } = useExitingTabItems(tabs, getId);
67
+ /**
68
+ * Stable sortable ids for open tabs.
69
+ */
70
+ const sortableIds = useMemo(() => tabs.map((tab) => tabSortableId(resolvedSortablePrefix, tab.id)), [tabs, resolvedSortablePrefix]);
71
+ /**
72
+ * Stable tab ids in display order for context menu close actions.
73
+ */
74
+ const orderedIds = useMemo(() => tabs.map((tab) => tab.id), [tabs]);
75
+ /**
76
+ * Menu groups for the open tab context menu, when one is visible.
77
+ */
78
+ const contextMenuGroups = useMemo(() => {
79
+ if (contextMenu == null || buildContextMenuGroups == null) {
80
+ return [];
81
+ }
82
+ return buildContextMenuGroups(contextMenu.targetId, orderedIds);
83
+ }, [buildContextMenuGroups, contextMenu, orderedIds]);
84
+ /**
85
+ * Moves focus to the active tab when the focused tab row was just removed.
86
+ */
87
+ useEffect(() => {
88
+ if (removedIds.length === 0 || onFocusTab == null) {
89
+ return;
90
+ }
91
+ const focusedTab = document.activeElement?.closest('[role="tab"]');
92
+ if (!(focusedTab instanceof HTMLElement) || !focusedTab.id.startsWith(tabIdPrefix)) {
93
+ return;
94
+ }
95
+ const focusedTabId = focusedTab.id.slice(tabIdPrefix.length);
96
+ if (!removedIds.some((id) => String(id) === focusedTabId)) {
97
+ return;
98
+ }
99
+ requestAnimationFrame(() => {
100
+ onFocusTab(activeId);
101
+ });
102
+ }, [activeId, onFocusTab, removedIds, tabIdPrefix]);
103
+ const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 6 } }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }));
104
+ /**
105
+ * Tab currently being dragged for overlay preview.
106
+ */
107
+ const activeDragTab = useMemo(() => {
108
+ if (activeDragId == null) {
109
+ return null;
110
+ }
111
+ return tabs.find((tab) => tab.id === activeDragId) ?? null;
112
+ }, [activeDragId, tabs]);
113
+ /**
114
+ * Records the tab being dragged for overlay preview.
115
+ */
116
+ const handleDragStart = (event) => {
117
+ const tabId = parseTabSortableId(resolvedSortablePrefix, String(event.active.id));
118
+ if (tabId == null) {
119
+ setActiveDragId(null);
120
+ return;
121
+ }
122
+ const match = tabs.find((tab) => String(tab.id) === tabId);
123
+ setActiveDragId(match?.id ?? null);
124
+ };
125
+ /**
126
+ * Persists a new tab order when a tab is dropped.
127
+ */
128
+ const handleDragEnd = (event) => {
129
+ const { active, over } = event;
130
+ setActiveDragId(null);
131
+ if (!over || !sortableEnabled) {
132
+ return;
133
+ }
134
+ const activeTabIdFromDrag = parseTabSortableId(resolvedSortablePrefix, String(active.id));
135
+ const overTabId = parseTabSortableId(resolvedSortablePrefix, String(over.id));
136
+ if (activeTabIdFromDrag == null || overTabId == null || activeTabIdFromDrag === overTabId) {
137
+ return;
138
+ }
139
+ const tabIds = tabs.map((tab) => tab.id);
140
+ const oldIndex = tabIds.findIndex((id) => String(id) === activeTabIdFromDrag);
141
+ const newIndex = tabIds.findIndex((id) => String(id) === overTabId);
142
+ if (oldIndex < 0 || newIndex < 0) {
143
+ return;
144
+ }
145
+ onReorder(arrayMove(tabIds, oldIndex, newIndex));
146
+ };
147
+ /**
148
+ * Moves focus and selection across open tabs with arrow, Home, and End keys.
149
+ */
150
+ const handleTabListKeyDown = (event) => {
151
+ if (event.key === 'ArrowDown' && onArrowDownIntoPanel != null) {
152
+ const activeElement = document.activeElement;
153
+ if (activeElement instanceof HTMLElement) {
154
+ const tabElement = activeElement.closest('[role="tab"]');
155
+ if (tabElement instanceof HTMLElement && tabElement.id.startsWith(tabIdPrefix)) {
156
+ const tabId = tabElement.id.slice(tabIdPrefix.length);
157
+ const match = tabs.find((tab) => String(tab.id) === tabId);
158
+ if (match != null && onArrowDownIntoPanel(match.id)) {
159
+ event.preventDefault();
160
+ return;
161
+ }
162
+ }
163
+ }
164
+ }
165
+ const currentIndex = resolveFocusedTabIndex(tabs, activeId, tabIdPrefix);
166
+ if (currentIndex < 0) {
167
+ return;
168
+ }
169
+ const nextIndex = resolveTabListKeyAction(event.key, currentIndex, tabs.length);
170
+ if (nextIndex === null) {
171
+ return;
172
+ }
173
+ event.preventDefault();
174
+ const nextTab = tabs[nextIndex];
175
+ onSelect(nextTab.id);
176
+ if (onFocusTab != null) {
177
+ requestAnimationFrame(() => {
178
+ onFocusTab(nextTab.id);
179
+ });
180
+ }
181
+ };
182
+ const tabRowClassName = wrap ? 'w-full min-w-0 py-1' : 'flex w-max flex-nowrap items-end py-1';
183
+ const tabListClassName = wrap
184
+ ? 'flex min-w-0 w-full flex-wrap items-end gap-y-2'
185
+ : 'flex items-end';
186
+ const sortStrategy = wrap ? rectSortingStrategy : horizontalListSortingStrategy;
187
+ const newTabButton = (_jsx(TabNewButton, { wrapped: wrap, ariaLabel: newTab.ariaLabel, title: newTab.title, onClick: newTab.onClick }));
188
+ /**
189
+ * Renders one tab shell, optionally inside a close-animation wrapper.
190
+ */
191
+ const renderTabShell = (item, options) => (_jsx(TabBarShell, { item: item, tabIndex: options.tabIndex, sortableId: tabSortableId(resolvedSortablePrefix, item.id), sortableDisabled: options.sortableDisabled, exiting: options.exiting, tabIdPrefix: tabIdPrefix, panelIdPrefix: panelIdPrefix, maxTabWidthClass: maxTabWidthClass, sortableCursor: sortableCursor, onSelect: onSelect, onClose: onClose, onContextMenu: options.onContextMenu }));
192
+ const tabRow = (_jsxs("div", { className: tabRowClassName, children: [_jsxs(DndContext, { sensors: sensors, collisionDetection: closestCenter, onDragStart: handleDragStart, onDragEnd: handleDragEnd, onDragCancel: () => setActiveDragId(null), children: [_jsxs("div", { role: "tablist", "aria-label": ariaLabel, className: tabListClassName, onKeyDown: handleTabListKeyDown, children: [_jsxs(SortableContext, { items: sortableIds, strategy: sortStrategy, children: [tabs.flatMap((tab) => {
193
+ const nodes = getExitingBefore(tab.id).map((exitingTab) => (_jsx(ClosingTabShell, { onComplete: () => completeExit(exitingTab.exitKey), children: renderTabShell(exitingTab.item, {
194
+ tabIndex: -1,
195
+ sortableDisabled: true,
196
+ exiting: true
197
+ }) }, exitingTab.exitKey)));
198
+ nodes.push(_jsx(TabBarShell, { item: tab, tabIndex: 0, sortableId: tabSortableId(resolvedSortablePrefix, tab.id), sortableDisabled: !sortableEnabled, tabIdPrefix: tabIdPrefix, panelIdPrefix: panelIdPrefix, maxTabWidthClass: maxTabWidthClass, sortableCursor: sortableCursor, onSelect: onSelect, onClose: onClose, onContextMenu: buildContextMenuGroups == null
199
+ ? undefined
200
+ : (id, event) => {
201
+ setContextMenu({
202
+ targetId: id,
203
+ x: event.clientX,
204
+ y: event.clientY
205
+ });
206
+ } }, `live-${String(tab.id)}`));
207
+ return nodes;
208
+ }), getExitingBefore(null).map((exitingTab) => (_jsx(ClosingTabShell, { onComplete: () => completeExit(exitingTab.exitKey), children: renderTabShell(exitingTab.item, {
209
+ tabIndex: -1,
210
+ sortableDisabled: true,
211
+ exiting: true
212
+ }) }, exitingTab.exitKey)))] }), wrap ? newTabButton : null] }), _jsx(DragOverlay, { children: activeDragTab ? (_jsx("div", { className: "rounded-t-lg border border-separator bg-surface px-3 py-2 text-[14px] font-medium shadow-md", children: activeDragTab.dragLabel })) : null })] }), wrap ? null : newTabButton] }));
213
+ const defaultScrollContainer = (row) => (_jsx("div", { className: "min-w-0 flex-1 overflow-x-auto", children: row }));
214
+ const scrollContainer = renderScrollContainer ?? defaultScrollContainer;
215
+ const containerClassName = cn('app-no-drag flex shrink-0 items-end border-b border-separator bg-sidebar px-2', className);
216
+ return (_jsxs("div", { className: containerClassName, children: [wrap ? _jsx("div", { className: "min-w-0 flex-1", children: tabRow }) : scrollContainer(tabRow), contextMenu != null && buildContextMenuGroups != null && (_jsx(TabContextMenu, { groups: contextMenuGroups, position: { x: contextMenu.x, y: contextMenu.y }, onClose: () => setContextMenu(null) }))] }));
217
+ }
218
+ export { TabNewButton } from './TabNewButton.js';
219
+ export { TabContextMenu } from './TabContextMenu.js';
220
+ export { ClosingTabShell } from './ClosingTabShell.js';
221
+ export { TabBarShell } from './TabBarShell.js';
222
+ export { useSortableTabItem } from './useSortableTabItem.js';
223
+ export { useExitingTabItems, detectRemovedTabItems, resolveInsertBeforeId } from './useExitingTabItems.js';
224
+ export { buildTabCloseMenuGroups, tabIdsToCloseOthers, tabIdsToCloseToTheRight } from './tabCloseMenuHelpers.js';
@@ -0,0 +1,41 @@
1
+ import type { MenuItem } from '../RowActionsMenu/index.js';
2
+ interface TabCloseMenuActions<T extends string | number> {
3
+ /**
4
+ * Closes a single tab.
5
+ */
6
+ onClose: (id: T) => void;
7
+ /**
8
+ * Closes every tab in `ids`.
9
+ */
10
+ onCloseMany: (ids: T[]) => void;
11
+ /**
12
+ * Closes tabs that are considered saved (non-dirty for requests, with messages for AI).
13
+ */
14
+ onCloseSaved: () => void;
15
+ }
16
+ /**
17
+ * Builds grouped context menu items shared by request and AI tab bars.
18
+ *
19
+ * @param orderedIds - Tab ids in display order.
20
+ * @param targetId - Tab that was right-clicked.
21
+ * @param actions - Close handlers for single, bulk, and saved-only operations.
22
+ */
23
+ export declare function buildTabCloseMenuGroups<T extends string | number>(orderedIds: readonly T[], targetId: T, actions: TabCloseMenuActions<T>): MenuItem[][];
24
+ /**
25
+ * Returns tab ids for every tab except the one at `targetId`.
26
+ *
27
+ * @param orderedIds - Tab ids in display order.
28
+ * @param targetId - Tab that should remain open.
29
+ * @returns Ids of all other tabs.
30
+ */
31
+ export declare function tabIdsToCloseOthers<T extends string | number>(orderedIds: readonly T[], targetId: T): T[];
32
+ /**
33
+ * Returns tab ids positioned to the right of `targetId` in display order.
34
+ *
35
+ * @param orderedIds - Tab ids in display order.
36
+ * @param targetId - Tab whose right-hand neighbors should be closed.
37
+ * @returns Ids of tabs after `targetId`, or an empty array when not found.
38
+ */
39
+ export declare function tabIdsToCloseToTheRight<T extends string | number>(orderedIds: readonly T[], targetId: T): T[];
40
+ export {};
41
+ //# sourceMappingURL=tabCloseMenuHelpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tabCloseMenuHelpers.d.ts","sourceRoot":"","sources":["../../../src/components/TabBar/tabCloseMenuHelpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,4BAA4B,CAAC;AAE3D,UAAU,mBAAmB,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM;IACrD;;OAEG;IACH,OAAO,EAAE,CAAC,EAAE,EAAE,CAAC,KAAK,IAAI,CAAC;IAEzB;;OAEG;IACH,WAAW,EAAE,CAAC,GAAG,EAAE,CAAC,EAAE,KAAK,IAAI,CAAC;IAEhC;;OAEG;IACH,YAAY,EAAE,MAAM,IAAI,CAAC;CAC1B;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,EAC/D,UAAU,EAAE,SAAS,CAAC,EAAE,EACxB,QAAQ,EAAE,CAAC,EACX,OAAO,EAAE,mBAAmB,CAAC,CAAC,CAAC,GAC9B,QAAQ,EAAE,EAAE,CAuBd;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,EAC3D,UAAU,EAAE,SAAS,CAAC,EAAE,EACxB,QAAQ,EAAE,CAAC,GACV,CAAC,EAAE,CAEL;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,EAC/D,UAAU,EAAE,SAAS,CAAC,EAAE,EACxB,QAAQ,EAAE,CAAC,GACV,CAAC,EAAE,CAML"}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Builds grouped context menu items shared by request and AI tab bars.
3
+ *
4
+ * @param orderedIds - Tab ids in display order.
5
+ * @param targetId - Tab that was right-clicked.
6
+ * @param actions - Close handlers for single, bulk, and saved-only operations.
7
+ */
8
+ export function buildTabCloseMenuGroups(orderedIds, targetId, actions) {
9
+ const targetIndex = orderedIds.indexOf(targetId);
10
+ const others = tabIdsToCloseOthers(orderedIds, targetId);
11
+ const toTheRight = tabIdsToCloseToTheRight(orderedIds, targetId);
12
+ const items = [{ label: 'Close', onSelect: () => actions.onClose(targetId) }];
13
+ if (orderedIds.length > 1) {
14
+ items.push({ label: 'Close others', onSelect: () => actions.onCloseMany(others) });
15
+ }
16
+ if (targetIndex >= 0 && targetIndex < orderedIds.length - 1) {
17
+ items.push({
18
+ label: 'Close to the right',
19
+ onSelect: () => actions.onCloseMany(toTheRight)
20
+ });
21
+ }
22
+ items.push({ label: 'Close saved', onSelect: () => actions.onCloseSaved() }, { label: 'Close all', onSelect: () => actions.onCloseMany([...orderedIds]) });
23
+ return [items];
24
+ }
25
+ /**
26
+ * Returns tab ids for every tab except the one at `targetId`.
27
+ *
28
+ * @param orderedIds - Tab ids in display order.
29
+ * @param targetId - Tab that should remain open.
30
+ * @returns Ids of all other tabs.
31
+ */
32
+ export function tabIdsToCloseOthers(orderedIds, targetId) {
33
+ return orderedIds.filter((id) => id !== targetId);
34
+ }
35
+ /**
36
+ * Returns tab ids positioned to the right of `targetId` in display order.
37
+ *
38
+ * @param orderedIds - Tab ids in display order.
39
+ * @param targetId - Tab whose right-hand neighbors should be closed.
40
+ * @returns Ids of tabs after `targetId`, or an empty array when not found.
41
+ */
42
+ export function tabIdsToCloseToTheRight(orderedIds, targetId) {
43
+ const index = orderedIds.indexOf(targetId);
44
+ if (index < 0) {
45
+ return [];
46
+ }
47
+ return orderedIds.slice(index + 1);
48
+ }