@harborclient/sdk 1.0.66 → 1.0.68
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/FooterButton/index.d.ts +2 -2
- package/dist/components/FooterButton/index.d.ts.map +1 -1
- package/dist/components/FooterButton/index.js +1 -1
- package/dist/components/FooterIcon/index.d.ts +2 -1
- package/dist/components/FooterIcon/index.d.ts.map +1 -1
- package/dist/components/FooterIcon/index.js +3 -2
- package/dist/components/FooterPanel/index.d.ts +50 -0
- package/dist/components/FooterPanel/index.d.ts.map +1 -0
- package/dist/components/FooterPanel/index.js +12 -0
- package/dist/components/Resizable/Resizable.d.ts +5 -1
- package/dist/components/Resizable/Resizable.d.ts.map +1 -1
- package/dist/components/Resizable/Resizable.js +2 -2
- package/dist/components/TabBar/ClosingTabShell.d.ts +20 -0
- package/dist/components/TabBar/ClosingTabShell.d.ts.map +1 -0
- package/dist/components/TabBar/ClosingTabShell.js +126 -0
- package/dist/components/TabBar/TabBarShell.d.ts +58 -0
- package/dist/components/TabBar/TabBarShell.d.ts.map +1 -0
- package/dist/components/TabBar/TabBarShell.js +37 -0
- package/dist/components/TabBar/TabContextMenu.d.ts +32 -0
- package/dist/components/TabBar/TabContextMenu.d.ts.map +1 -0
- package/dist/components/TabBar/TabContextMenu.js +133 -0
- package/dist/components/TabBar/TabNewButton.d.ts +26 -0
- package/dist/components/TabBar/TabNewButton.d.ts.map +1 -0
- package/dist/components/TabBar/TabNewButton.js +14 -0
- package/dist/components/TabBar/index.d.ts +92 -0
- package/dist/components/TabBar/index.d.ts.map +1 -0
- package/dist/components/TabBar/index.js +224 -0
- package/dist/components/TabBar/tabCloseMenuHelpers.d.ts +41 -0
- package/dist/components/TabBar/tabCloseMenuHelpers.d.ts.map +1 -0
- package/dist/components/TabBar/tabCloseMenuHelpers.js +48 -0
- package/dist/components/TabBar/types.d.ts +60 -0
- package/dist/components/TabBar/types.d.ts.map +1 -0
- package/dist/components/TabBar/types.js +1 -0
- package/dist/components/TabBar/useExitingTabItems.d.ts +73 -0
- package/dist/components/TabBar/useExitingTabItems.d.ts.map +1 -0
- package/dist/components/TabBar/useExitingTabItems.js +103 -0
- package/dist/components/TabBar/useSortableTabItem.d.ts +31 -0
- package/dist/components/TabBar/useSortableTabItem.d.ts.map +1 -0
- package/dist/components/TabBar/useSortableTabItem.js +29 -0
- package/dist/components/Toolbar/index.d.ts.map +1 -1
- package/dist/components/Toolbar/index.js +21 -5
- package/dist/components/classes.d.ts +9 -0
- package/dist/components/classes.d.ts.map +1 -1
- package/dist/components/classes.js +15 -2
- package/dist/components/footerBarUtils.d.ts +11 -0
- package/dist/components/footerBarUtils.d.ts.map +1 -0
- package/dist/components/footerBarUtils.js +10 -0
- package/dist/components/index.d.ts +4 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +4 -1
- package/dist/styles.css +12 -0
- package/dist/types.d.ts +2 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +7 -1
|
@@ -3,11 +3,11 @@ interface Props extends Omit<ComponentPropsWithoutRef<'button'>, 'children' | 'a
|
|
|
3
3
|
/**
|
|
4
4
|
* Whether the associated slide-up panel is currently open.
|
|
5
5
|
*/
|
|
6
|
-
active
|
|
6
|
+
active?: boolean;
|
|
7
7
|
/**
|
|
8
8
|
* Panel element id referenced by `aria-controls`.
|
|
9
9
|
*/
|
|
10
|
-
controlsId
|
|
10
|
+
controlsId?: string;
|
|
11
11
|
/**
|
|
12
12
|
* Button label and optional count badge content.
|
|
13
13
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/FooterButton/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,wBAAwB,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAGtE,UAAU,KAAM,SAAQ,IAAI,CAC1B,wBAAwB,CAAC,QAAQ,CAAC,EAClC,UAAU,GAAG,eAAe,GAAG,eAAe,CAC/C;IACC;;OAEG;IACH,MAAM,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/FooterButton/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,wBAAwB,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAGtE,UAAU,KAAM,SAAQ,IAAI,CAC1B,wBAAwB,CAAC,QAAQ,CAAC,EAClC,UAAU,GAAG,eAAe,GAAG,eAAe,CAC/C;IACC;;OAEG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IAEjB;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;OAEG;IACH,QAAQ,EAAE,SAAS,CAAC;CACrB;AAWD;;;GAGG;AACH,wBAAgB,YAAY,CAAC,EAC3B,MAAc,EACd,OAAO,EACP,UAAU,EACV,QAAQ,EACR,SAAS,EACT,GAAG,KAAK,EACT,EAAE,KAAK,GAAG,GAAG,CAAC,OAAO,CAarB"}
|
|
@@ -12,6 +12,6 @@ function footerSegment(active) {
|
|
|
12
12
|
* Text segment toggle button for the window footer bar, used to open and close
|
|
13
13
|
* slide-up panels such as Console and Variables.
|
|
14
14
|
*/
|
|
15
|
-
export function FooterButton({ active, onClick, controlsId, children, className, ...props }) {
|
|
15
|
+
export function FooterButton({ active = false, onClick, controlsId, children, className, ...props }) {
|
|
16
16
|
return (_jsx("button", { ...props, type: "button", className: cn('hc-footer-button', footerSegment(active), className), onClick: onClick, "aria-expanded": active, "aria-controls": controlsId, children: children }));
|
|
17
17
|
}
|
|
@@ -24,7 +24,8 @@ interface Props extends Omit<ComponentPropsWithoutRef<'button'>, 'aria-label' |
|
|
|
24
24
|
title?: string;
|
|
25
25
|
/**
|
|
26
26
|
* Active-state background treatment. Use `"selection"` to match sidebar
|
|
27
|
-
* toolbar icon toggles; defaults to `"surface"` for
|
|
27
|
+
* toolbar icon toggles (`bg-sidebar-section`); defaults to `"surface"` for
|
|
28
|
+
* the action menu icon.
|
|
28
29
|
*/
|
|
29
30
|
activeStyle?: ActiveStyle;
|
|
30
31
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/FooterIcon/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mCAAmC,CAAC;AACxE,OAAO,KAAK,EAAE,wBAAwB,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/FooterIcon/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mCAAmC,CAAC;AACxE,OAAO,KAAK,EAAE,wBAAwB,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC;AAK3D;;GAEG;AACH,KAAK,WAAW,GAAG,SAAS,GAAG,WAAW,CAAC;AAE3C,UAAU,KAAM,SAAQ,IAAI,CAAC,wBAAwB,CAAC,QAAQ,CAAC,EAAE,YAAY,GAAG,cAAc,CAAC;IAC7F;;OAEG;IACH,IAAI,EAAE,cAAc,CAAC;IAErB;;OAEG;IACH,MAAM,EAAE,OAAO,CAAC;IAEhB;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;OAEG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;;OAIG;IACH,WAAW,CAAC,EAAE,WAAW,CAAC;CAC3B;AAuBD;;;GAGG;AACH,wBAAgB,UAAU,CAAC,EACzB,IAAI,EACJ,MAAM,EACN,OAAO,EACP,KAAK,EACL,KAAK,EACL,WAAuB,EACvB,SAAS,EACT,GAAG,KAAK,EACT,EAAE,KAAK,GAAG,GAAG,CAAC,OAAO,CAqBrB"}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx } from "@harborclient/sdk/jsx-runtime";
|
|
2
2
|
import { FaIcon } from '../FaIcon/index.js';
|
|
3
|
+
import { footerIconButtonSizeClass } from '../footerBarUtils.js';
|
|
3
4
|
import { cn } from '../utils.js';
|
|
4
|
-
const footerIconBase =
|
|
5
|
+
const footerIconBase = `inline-flex ${footerIconButtonSizeClass} shrink-0 cursor-pointer items-center justify-center rounded-md border-none app-no-drag`;
|
|
5
6
|
/**
|
|
6
7
|
* Square icon toggle styles for footer sidebar buttons.
|
|
7
8
|
*
|
|
@@ -12,7 +13,7 @@ const footerIconBase = 'inline-flex h-7 w-7 shrink-0 cursor-pointer items-center
|
|
|
12
13
|
function footerIconButton(active, activeStyle) {
|
|
13
14
|
if (activeStyle === 'selection') {
|
|
14
15
|
return active
|
|
15
|
-
? `${footerIconBase} bg-
|
|
16
|
+
? `${footerIconBase} bg-sidebar-section text-text`
|
|
16
17
|
: `${footerIconBase} bg-transparent text-text hover:bg-selection focus-visible:bg-selection focus-visible:text-text`;
|
|
17
18
|
}
|
|
18
19
|
return active
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import React, { type JSX } from 'react';
|
|
2
|
+
interface Props {
|
|
3
|
+
id: string;
|
|
4
|
+
/**
|
|
5
|
+
* The title of the panel.
|
|
6
|
+
*/
|
|
7
|
+
title?: React.ReactNode;
|
|
8
|
+
/**
|
|
9
|
+
* Displayed next to the title in smaller muted text.
|
|
10
|
+
*/
|
|
11
|
+
badge?: React.ReactNode;
|
|
12
|
+
/**
|
|
13
|
+
* Displayed under the title.
|
|
14
|
+
*/
|
|
15
|
+
description?: React.ReactNode;
|
|
16
|
+
/**
|
|
17
|
+
* Buttons to display in the footer.
|
|
18
|
+
*/
|
|
19
|
+
buttons?: React.ReactNode[];
|
|
20
|
+
/**
|
|
21
|
+
* The label for the close button.
|
|
22
|
+
*/
|
|
23
|
+
closeLabel: string;
|
|
24
|
+
/**
|
|
25
|
+
* The memory storage key.
|
|
26
|
+
*/
|
|
27
|
+
storageKey: string;
|
|
28
|
+
/**
|
|
29
|
+
* Whether the panel is visible (slides up when true).
|
|
30
|
+
*/
|
|
31
|
+
open: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Closes the variables panel.
|
|
34
|
+
*/
|
|
35
|
+
onClose: () => void;
|
|
36
|
+
/**
|
|
37
|
+
* The contents of the panel.
|
|
38
|
+
*/
|
|
39
|
+
children: React.ReactNode;
|
|
40
|
+
/**
|
|
41
|
+
* When true, does not render children while the panel is closed.
|
|
42
|
+
*/
|
|
43
|
+
unmountWhenClosed?: boolean;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Resizable panel that slides up from the footer.
|
|
47
|
+
*/
|
|
48
|
+
export declare function FooterPanel({ id, title, badge, description, closeLabel, storageKey, buttons, open, onClose, children, unmountWhenClosed }: Props): JSX.Element;
|
|
49
|
+
export {};
|
|
50
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/FooterPanel/index.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,EAAE,KAAK,GAAG,EAAE,MAAM,OAAO,CAAC;AAKxC,UAAU,KAAK;IACb,EAAE,EAAE,MAAM,CAAC;IAEX;;OAEG;IACH,KAAK,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAExB;;OAEG;IACH,KAAK,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAExB;;OAEG;IACH,WAAW,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAE9B;;OAEG;IACH,OAAO,CAAC,EAAE,KAAK,CAAC,SAAS,EAAE,CAAC;IAE5B;;OAEG;IACH,UAAU,EAAE,MAAM,CAAC;IAEnB;;OAEG;IACH,UAAU,EAAE,MAAM,CAAC;IAEnB;;OAEG;IACH,IAAI,EAAE,OAAO,CAAC;IAEd;;OAEG;IACH,OAAO,EAAE,MAAM,IAAI,CAAC;IAEpB;;OAEG;IACH,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAE1B;;OAEG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,EAC1B,EAAE,EACF,KAAK,EACL,KAAK,EACL,WAAW,EACX,UAAU,EACV,UAAU,EACV,OAAO,EACP,IAAI,EACJ,OAAO,EACP,QAAQ,EACR,iBAAyB,EAC1B,EAAE,KAAK,GAAG,GAAG,CAAC,OAAO,CA6CrB"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "@harborclient/sdk/jsx-runtime";
|
|
2
|
+
import { faXmark } from '@fortawesome/free-solid-svg-icons';
|
|
3
|
+
import { FaIcon } from '../FaIcon/index.js';
|
|
4
|
+
import { Resizable } from '../index';
|
|
5
|
+
import { cn } from '../utils.js';
|
|
6
|
+
/**
|
|
7
|
+
* Resizable panel that slides up from the footer.
|
|
8
|
+
*/
|
|
9
|
+
export function FooterPanel({ id, title, badge, description, closeLabel, storageKey, buttons, open, onClose, children, unmountWhenClosed = false }) {
|
|
10
|
+
const closeButton = (_jsx("button", { type: "button", title: "Close", className: cn('hc-footer-panel-close cursor-pointer'), onClick: onClose, "aria-label": `Close ${closeLabel}`, children: _jsx(FaIcon, { icon: faXmark, className: "h-3.5 w-3.5" }) }));
|
|
11
|
+
return (_jsxs(Resizable, { id: id, open: open, onClose: onClose, closeLabel: closeLabel, storageKey: storageKey, unmountWhenClosed: unmountWhenClosed, showCloseButton: false, headerless: true, children: [_jsxs("div", { className: "hc-footer-panel flex shrink-0 items-center justify-between border-b border-separator px-3 py-2", children: [_jsxs("div", { children: [_jsxs("div", { className: "flex items-center gap-2 text-[16px] font-medium text-text", children: [title, badge && (_jsx("span", { className: "hc-footer-panel-badge text-[16px] font-normal text-muted", children: badge }))] }), description && _jsx("span", { className: "truncate text-[16px] text-muted", children: description })] }), _jsxs("div", { className: "flex gap-2", children: [buttons, closeButton] })] }), _jsx("div", { className: "hc-footer-panel-body min-h-0 flex-1 overflow-auto", children: children })] }));
|
|
12
|
+
}
|
|
@@ -32,6 +32,10 @@ interface Props extends Omit<ComponentPropsWithoutRef<'div'>, 'children' | 'id'
|
|
|
32
32
|
* When true, does not render children while the panel is closed.
|
|
33
33
|
*/
|
|
34
34
|
unmountWhenClosed?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Whether or not to show the close button.
|
|
37
|
+
*/
|
|
38
|
+
showCloseButton?: boolean;
|
|
35
39
|
/**
|
|
36
40
|
* Panel body content.
|
|
37
41
|
*/
|
|
@@ -43,6 +47,6 @@ interface Props extends Omit<ComponentPropsWithoutRef<'div'>, 'children' | 'id'
|
|
|
43
47
|
* Encapsulates resize handle wiring, height persistence, open/close animation,
|
|
44
48
|
* and close affordance shared by Console, Variables, and plugin footer panels.
|
|
45
49
|
*/
|
|
46
|
-
export declare function Resizable({ id, open, onClose, closeLabel, storageKey, title, headerless, unmountWhenClosed, children, className, ...props }: Props): JSX.Element;
|
|
50
|
+
export declare function Resizable({ id, open, onClose, closeLabel, storageKey, title, headerless, unmountWhenClosed, showCloseButton, children, className, ...props }: Props): JSX.Element;
|
|
47
51
|
export {};
|
|
48
52
|
//# sourceMappingURL=Resizable.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Resizable.d.ts","sourceRoot":"","sources":["../../../src/components/Resizable/Resizable.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,wBAAwB,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAatE,UAAU,KAAM,SAAQ,IAAI,CAAC,wBAAwB,CAAC,KAAK,CAAC,EAAE,UAAU,GAAG,IAAI,GAAG,OAAO,CAAC;IACxF;;OAEG;IACH,EAAE,EAAE,MAAM,CAAC;IAEX;;OAEG;IACH,IAAI,EAAE,OAAO,CAAC;IAEd;;OAEG;IACH,OAAO,EAAE,MAAM,IAAI,CAAC;IAEpB;;OAEG;IACH,UAAU,EAAE,MAAM,CAAC;IAEnB;;OAEG;IACH,UAAU,EAAE,MAAM,CAAC;IAEnB;;OAEG;IACH,KAAK,CAAC,EAAE,SAAS,CAAC;IAElB;;OAEG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;OAEG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAE5B;;OAEG;IACH,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,EACxB,EAAE,EACF,IAAI,EACJ,OAAO,EACP,UAAU,EACV,UAAU,EACV,KAAK,EACL,UAAkB,EAClB,iBAAyB,EACzB,QAAQ,EACR,SAAS,EACT,GAAG,KAAK,EACT,EAAE,KAAK,GAAG,GAAG,CAAC,OAAO,
|
|
1
|
+
{"version":3,"file":"Resizable.d.ts","sourceRoot":"","sources":["../../../src/components/Resizable/Resizable.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,wBAAwB,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAatE,UAAU,KAAM,SAAQ,IAAI,CAAC,wBAAwB,CAAC,KAAK,CAAC,EAAE,UAAU,GAAG,IAAI,GAAG,OAAO,CAAC;IACxF;;OAEG;IACH,EAAE,EAAE,MAAM,CAAC;IAEX;;OAEG;IACH,IAAI,EAAE,OAAO,CAAC;IAEd;;OAEG;IACH,OAAO,EAAE,MAAM,IAAI,CAAC;IAEpB;;OAEG;IACH,UAAU,EAAE,MAAM,CAAC;IAEnB;;OAEG;IACH,UAAU,EAAE,MAAM,CAAC;IAEnB;;OAEG;IACH,KAAK,CAAC,EAAE,SAAS,CAAC;IAElB;;OAEG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;OAEG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAE5B;;OAEG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAE1B;;OAEG;IACH,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,EACxB,EAAE,EACF,IAAI,EACJ,OAAO,EACP,UAAU,EACV,UAAU,EACV,KAAK,EACL,UAAkB,EAClB,iBAAyB,EACzB,eAAsB,EACtB,QAAQ,EACR,SAAS,EACT,GAAG,KAAK,EACT,EAAE,KAAK,GAAG,GAAG,CAAC,OAAO,CAuFrB"}
|
|
@@ -12,7 +12,7 @@ import { useResizable } from './useResizable.js';
|
|
|
12
12
|
* Encapsulates resize handle wiring, height persistence, open/close animation,
|
|
13
13
|
* and close affordance shared by Console, Variables, and plugin footer panels.
|
|
14
14
|
*/
|
|
15
|
-
export function Resizable({ id, open, onClose, closeLabel, storageKey, title, headerless = false, unmountWhenClosed = false, children, className, ...props }) {
|
|
15
|
+
export function Resizable({ id, open, onClose, closeLabel, storageKey, title, headerless = false, unmountWhenClosed = false, showCloseButton = true, children, className, ...props }) {
|
|
16
16
|
const containerRef = useRef(null);
|
|
17
17
|
/**
|
|
18
18
|
* Keeps max-size measurement stable across re-renders so useResizable
|
|
@@ -29,5 +29,5 @@ export function Resizable({ id, open, onClose, closeLabel, storageKey, title, he
|
|
|
29
29
|
});
|
|
30
30
|
const closeButton = (_jsx("button", { type: "button", className: cn('hc-resizable-close', footerPanelCloseButtonClassName), onClick: onClose, "aria-label": `Close ${closeLabel}`, children: _jsx(FaIcon, { icon: faXmark, className: "h-3.5 w-3.5" }) }));
|
|
31
31
|
const body = unmountWhenClosed && !open ? null : children;
|
|
32
|
-
return (_jsxs("div", { ref: containerRef, ...props, id: id, className: cn('hc-resizable', footerPanelClassName(open), className), style: { height }, role: "region", "aria-label": `${closeLabel} panel`, "aria-hidden": !open, inert: !open || undefined, children: [_jsx(ResizeHandle, { orientation: "horizontal", value: height, min: panelMinSize, max: panelMaxSize, onResizeStart: onResizeStart, onKeyboardResize: onKeyboardResize, ariaLabel: `Resize ${closeLabel} panel` }), headerless ? (_jsxs("div", { className: "hc-resizable-headerless relative flex min-h-0 flex-1 flex-col overflow-hidden", children: [_jsx("button", { type: "button", className: cn('hc-resizable-headerless-close absolute top-2 right-2 z-10', footerPanelCloseButtonClassName), onClick: onClose, "aria-label": `Close ${closeLabel}`, children: _jsx(FaIcon, { icon: faXmark, className: "h-3.5 w-3.5" }) }), _jsx("div", { className: "hc-resizable-body flex min-h-0 flex-1 flex-col", children: body })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "hc-resizable-header flex shrink-0 items-center justify-between border-b border-separator px-3 py-2", children: [_jsx("div", { className: "hc-resizable-header-title min-w-0 flex-1", children: title }), closeButton] }), body] }))] }));
|
|
32
|
+
return (_jsxs("div", { ref: containerRef, ...props, id: id, className: cn('hc-resizable', footerPanelClassName(open), className), style: { height }, role: "region", "aria-label": `${closeLabel} panel`, "aria-hidden": !open, inert: !open || undefined, children: [_jsx(ResizeHandle, { orientation: "horizontal", value: height, min: panelMinSize, max: panelMaxSize, onResizeStart: onResizeStart, onKeyboardResize: onKeyboardResize, ariaLabel: `Resize ${closeLabel} panel` }), headerless ? (_jsxs("div", { className: "hc-resizable-headerless relative flex min-h-0 flex-1 flex-col overflow-hidden", children: [showCloseButton && (_jsx("button", { type: "button", className: cn('hc-resizable-headerless-close absolute top-2 right-2 z-10', footerPanelCloseButtonClassName), onClick: onClose, "aria-label": `Close ${closeLabel}`, children: _jsx(FaIcon, { icon: faXmark, className: "h-3.5 w-3.5" }) })), _jsx("div", { className: "hc-resizable-body flex min-h-0 flex-1 flex-col", children: body })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "hc-resizable-header flex shrink-0 items-center justify-between border-b border-separator px-3 py-2", children: [_jsx("div", { className: "hc-resizable-header-title min-w-0 flex-1", children: title }), showCloseButton && closeButton] }), body] }))] }));
|
|
33
33
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { JSX, ReactNode } from 'react';
|
|
2
|
+
interface Props {
|
|
3
|
+
/**
|
|
4
|
+
* Tab row content to measure and collapse horizontally.
|
|
5
|
+
*/
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
/**
|
|
8
|
+
* Called after the close width transition finishes.
|
|
9
|
+
*/
|
|
10
|
+
onComplete: () => void;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Horizontally collapses a tab row with a quick width and opacity "zap out".
|
|
14
|
+
*
|
|
15
|
+
* Keeps children mounted until the width transition reaches zero so siblings
|
|
16
|
+
* do not reflow until the animation completes.
|
|
17
|
+
*/
|
|
18
|
+
export declare function ClosingTabShell({ children, onComplete }: Props): JSX.Element | null;
|
|
19
|
+
export {};
|
|
20
|
+
//# sourceMappingURL=ClosingTabShell.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ClosingTabShell.d.ts","sourceRoot":"","sources":["../../../src/components/TabBar/ClosingTabShell.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,GAAG,EAAE,SAAS,EAAsD,MAAM,OAAO,CAAC;AAEhG,UAAU,KAAK;IACb;;OAEG;IACH,QAAQ,EAAE,SAAS,CAAC;IAEpB;;OAEG;IACH,UAAU,EAAE,MAAM,IAAI,CAAC;CACxB;AA+HD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,EAAE,QAAQ,EAAE,UAAU,EAAE,EAAE,KAAK,GAAG,GAAG,CAAC,OAAO,GAAG,IAAI,CA0CnF"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { jsx as _jsx } from "@harborclient/sdk/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from '@harborclient/sdk/react';
|
|
3
|
+
/**
|
|
4
|
+
* Returns whether the user prefers reduced motion.
|
|
5
|
+
*
|
|
6
|
+
* @returns True when the OS requests minimized animation.
|
|
7
|
+
*/
|
|
8
|
+
function prefersReducedMotion() {
|
|
9
|
+
return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Manages width collapse state for a single closing tab shell.
|
|
13
|
+
*
|
|
14
|
+
* @param onComplete - Invoked after the close animation finishes.
|
|
15
|
+
* @returns State and handlers for the animated shell.
|
|
16
|
+
*/
|
|
17
|
+
function useClosingTabShell(onComplete) {
|
|
18
|
+
const innerRef = useRef(null);
|
|
19
|
+
const completedRef = useRef(false);
|
|
20
|
+
const [collapsed, setCollapsed] = useState(false);
|
|
21
|
+
const [contentWidth, setContentWidth] = useState(0);
|
|
22
|
+
const [transitionEnabled, setTransitionEnabled] = useState(false);
|
|
23
|
+
const [reducedMotion, setReducedMotion] = useState(prefersReducedMotion);
|
|
24
|
+
/**
|
|
25
|
+
* Subscribes to OS reduced-motion preference changes.
|
|
26
|
+
*/
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
|
|
29
|
+
/**
|
|
30
|
+
* Updates reduced-motion state when the OS preference changes.
|
|
31
|
+
*/
|
|
32
|
+
const handleChange = () => {
|
|
33
|
+
setReducedMotion(mediaQuery.matches);
|
|
34
|
+
};
|
|
35
|
+
mediaQuery.addEventListener('change', handleChange);
|
|
36
|
+
return () => mediaQuery.removeEventListener('change', handleChange);
|
|
37
|
+
}, []);
|
|
38
|
+
/**
|
|
39
|
+
* Measures the tab row so the outer shell can animate from its natural width to zero.
|
|
40
|
+
*/
|
|
41
|
+
useLayoutEffect(() => {
|
|
42
|
+
if (!innerRef.current) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const element = innerRef.current;
|
|
46
|
+
/**
|
|
47
|
+
* Writes the latest measured width from layout or resize observation.
|
|
48
|
+
*
|
|
49
|
+
* @param width - Measured inner width in pixels.
|
|
50
|
+
*/
|
|
51
|
+
const applyWidth = (width) => {
|
|
52
|
+
if (width > 0) {
|
|
53
|
+
setContentWidth(width);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
const observer = new ResizeObserver(([entry]) => {
|
|
57
|
+
applyWidth(entry.contentRect.width);
|
|
58
|
+
});
|
|
59
|
+
observer.observe(element);
|
|
60
|
+
applyWidth(element.getBoundingClientRect().width);
|
|
61
|
+
return () => observer.disconnect();
|
|
62
|
+
}, []);
|
|
63
|
+
/**
|
|
64
|
+
* Starts the collapse animation after the first paint, or completes immediately
|
|
65
|
+
* when reduced motion is enabled.
|
|
66
|
+
*/
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (reducedMotion) {
|
|
69
|
+
onComplete();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (contentWidth <= 0) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
requestAnimationFrame(() => {
|
|
76
|
+
setTransitionEnabled(true);
|
|
77
|
+
requestAnimationFrame(() => {
|
|
78
|
+
setCollapsed(true);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
}, [contentWidth, onComplete, reducedMotion]);
|
|
82
|
+
/**
|
|
83
|
+
* Finishes the exit animation once the width transition completes.
|
|
84
|
+
*/
|
|
85
|
+
const handleTransitionEnd = useCallback((event) => {
|
|
86
|
+
if (event.propertyName !== 'width' || completedRef.current) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
completedRef.current = true;
|
|
90
|
+
onComplete();
|
|
91
|
+
}, [onComplete]);
|
|
92
|
+
return {
|
|
93
|
+
collapsed,
|
|
94
|
+
contentWidth,
|
|
95
|
+
innerRef,
|
|
96
|
+
reducedMotion,
|
|
97
|
+
transitionEnabled,
|
|
98
|
+
handleTransitionEnd
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Horizontally collapses a tab row with a quick width and opacity "zap out".
|
|
103
|
+
*
|
|
104
|
+
* Keeps children mounted until the width transition reaches zero so siblings
|
|
105
|
+
* do not reflow until the animation completes.
|
|
106
|
+
*/
|
|
107
|
+
export function ClosingTabShell({ children, onComplete }) {
|
|
108
|
+
const { collapsed, contentWidth, innerRef, reducedMotion, transitionEnabled, handleTransitionEnd } = useClosingTabShell(onComplete);
|
|
109
|
+
if (reducedMotion) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
const outerClassName = [
|
|
113
|
+
'flex shrink-0 overflow-hidden',
|
|
114
|
+
transitionEnabled
|
|
115
|
+
? 'transition-[width] duration-150 ease-in motion-reduce:transition-none'
|
|
116
|
+
: 'transition-none'
|
|
117
|
+
].join(' ');
|
|
118
|
+
const innerClassName = [
|
|
119
|
+
'flex shrink-0',
|
|
120
|
+
transitionEnabled
|
|
121
|
+
? 'transition-[opacity,transform] duration-150 ease-in origin-right motion-reduce:transition-none'
|
|
122
|
+
: 'transition-none',
|
|
123
|
+
collapsed ? 'scale-x-95 opacity-0' : 'scale-x-100 opacity-100'
|
|
124
|
+
].join(' ');
|
|
125
|
+
return (_jsx("div", { className: outerClassName, style: { width: collapsed ? 0 : contentWidth }, onTransitionEnd: handleTransitionEnd, "aria-hidden": true, inert: true, children: _jsx("div", { ref: innerRef, className: innerClassName, children: children }) }));
|
|
126
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { JSX, MouseEvent } from 'react';
|
|
2
|
+
import type { TabBarItem, TabBarSortableCursor } from './types.js';
|
|
3
|
+
interface Props<TId extends string | number> {
|
|
4
|
+
/**
|
|
5
|
+
* Tab row data to render.
|
|
6
|
+
*/
|
|
7
|
+
item: TabBarItem<TId>;
|
|
8
|
+
/**
|
|
9
|
+
* Tab order index for the tab label; all tabs stay in sequential Tab order.
|
|
10
|
+
*/
|
|
11
|
+
tabIndex: number;
|
|
12
|
+
/**
|
|
13
|
+
* Stable dnd-kit sortable id for this tab row.
|
|
14
|
+
*/
|
|
15
|
+
sortableId: string;
|
|
16
|
+
/**
|
|
17
|
+
* When true, drag reordering is disabled for this tab.
|
|
18
|
+
*/
|
|
19
|
+
sortableDisabled?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* When true, renders a non-interactive snapshot for the close animation.
|
|
22
|
+
*/
|
|
23
|
+
exiting?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Prefix for tab element ids (for example `request-tab-`).
|
|
26
|
+
*/
|
|
27
|
+
tabIdPrefix: string;
|
|
28
|
+
/**
|
|
29
|
+
* Prefix for linked tab panel ids (for example `request-tabpanel-`).
|
|
30
|
+
*/
|
|
31
|
+
panelIdPrefix: string;
|
|
32
|
+
/**
|
|
33
|
+
* Maximum width class for the tab shell.
|
|
34
|
+
*/
|
|
35
|
+
maxTabWidthClass?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Cursor style when sortable drag is enabled.
|
|
38
|
+
*/
|
|
39
|
+
sortableCursor?: TabBarSortableCursor;
|
|
40
|
+
/**
|
|
41
|
+
* Called when the user selects this tab.
|
|
42
|
+
*/
|
|
43
|
+
onSelect: (id: TId) => void;
|
|
44
|
+
/**
|
|
45
|
+
* Called when the user closes this tab.
|
|
46
|
+
*/
|
|
47
|
+
onClose: (id: TId) => void;
|
|
48
|
+
/**
|
|
49
|
+
* Called when the user opens the tab context menu.
|
|
50
|
+
*/
|
|
51
|
+
onContextMenu?: (id: TId, event: MouseEvent<HTMLDivElement>) => void;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Single document-style tab row with close button and optional drag reordering.
|
|
55
|
+
*/
|
|
56
|
+
export declare function TabBarShell<TId extends string | number>({ item, tabIndex, sortableId, sortableDisabled, exiting, tabIdPrefix, panelIdPrefix, maxTabWidthClass, sortableCursor, onSelect, onClose, onContextMenu }: Props<TId>): JSX.Element;
|
|
57
|
+
export {};
|
|
58
|
+
//# sourceMappingURL=TabBarShell.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TabBarShell.d.ts","sourceRoot":"","sources":["../../../src/components/TabBar/TabBarShell.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAiB,UAAU,EAAE,MAAM,OAAO,CAAC;AAI5D,OAAO,KAAK,EAAE,UAAU,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAGnE,UAAU,KAAK,CAAC,GAAG,SAAS,MAAM,GAAG,MAAM;IACzC;;OAEG;IACH,IAAI,EAAE,UAAU,CAAC,GAAG,CAAC,CAAC;IAEtB;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAC;IAEjB;;OAEG;IACH,UAAU,EAAE,MAAM,CAAC;IAEnB;;OAEG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAE3B;;OAEG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB;;OAEG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;OAEG;IACH,aAAa,EAAE,MAAM,CAAC;IAEtB;;OAEG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAE1B;;OAEG;IACH,cAAc,CAAC,EAAE,oBAAoB,CAAC;IAEtC;;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,aAAa,CAAC,EAAE,CAAC,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,UAAU,CAAC,cAAc,CAAC,KAAK,IAAI,CAAC;CACtE;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,GAAG,SAAS,MAAM,GAAG,MAAM,EAAE,EACvD,IAAI,EACJ,QAAQ,EACR,UAAU,EACV,gBAAwB,EACxB,OAAe,EACf,WAAW,EACX,aAAa,EACb,gBAAkC,EAClC,cAA0B,EAC1B,QAAQ,EACR,OAAO,EACP,aAAa,EACd,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,OAAO,CA4E1B"}
|
|
@@ -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,32 @@
|
|
|
1
|
+
import type { JSX } from 'react';
|
|
2
|
+
import type { MenuItem } from '../RowActionsMenu/index.js';
|
|
3
|
+
interface Position {
|
|
4
|
+
/**
|
|
5
|
+
* Viewport X coordinate where the menu was opened.
|
|
6
|
+
*/
|
|
7
|
+
x: number;
|
|
8
|
+
/**
|
|
9
|
+
* Viewport Y coordinate where the menu was opened.
|
|
10
|
+
*/
|
|
11
|
+
y: number;
|
|
12
|
+
}
|
|
13
|
+
interface Props {
|
|
14
|
+
/**
|
|
15
|
+
* Grouped menu entries. Each inner array is one visual group separated by a divider.
|
|
16
|
+
*/
|
|
17
|
+
groups: MenuItem[][];
|
|
18
|
+
/**
|
|
19
|
+
* Cursor position used to anchor the menu panel.
|
|
20
|
+
*/
|
|
21
|
+
position: Position;
|
|
22
|
+
/**
|
|
23
|
+
* Called when the menu should close without selecting an item.
|
|
24
|
+
*/
|
|
25
|
+
onClose: () => void;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Cursor-positioned context menu for tab bars, rendered in a portal at the click point.
|
|
29
|
+
*/
|
|
30
|
+
export declare function TabContextMenu({ groups, position, onClose }: Props): JSX.Element | null;
|
|
31
|
+
export {};
|
|
32
|
+
//# 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;AAI3D,UAAU,QAAQ;IAChB;;OAEG;IACH,CAAC,EAAE,MAAM,CAAC;IAEV;;OAEG;IACH,CAAC,EAAE,MAAM,CAAC;CACX;AAED,UAAU,KAAK;IACb;;OAEG;IACH,MAAM,EAAE,QAAQ,EAAE,EAAE,CAAC;IAErB;;OAEG;IACH,QAAQ,EAAE,QAAQ,CAAC;IAEnB;;OAEG;IACH,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAgCD;;GAEG;AACH,wBAAgB,cAAc,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,KAAK,GAAG,GAAG,CAAC,OAAO,GAAG,IAAI,CAkJvF"}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { jsx as _jsx } from "@harborclient/sdk/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from '@harborclient/sdk/react';
|
|
3
|
+
import { portalToBody } from '../portalToBody.js';
|
|
4
|
+
import { cn, resolveTabListKeyAction } from '../utils.js';
|
|
5
|
+
/**
|
|
6
|
+
* Tailwind classes for a single menu item button.
|
|
7
|
+
*
|
|
8
|
+
* @param variant - Visual variant for default or destructive actions.
|
|
9
|
+
*/
|
|
10
|
+
function menuItemClass(variant) {
|
|
11
|
+
const base = 'block w-full cursor-pointer border-none bg-transparent px-3.5 py-1.5 text-left text-[16px] app-no-drag';
|
|
12
|
+
return variant === 'danger'
|
|
13
|
+
? `${base} text-text hover:bg-danger/15 hover:text-danger`
|
|
14
|
+
: `${base} text-text hover:bg-selection`;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Clamps a menu position so the panel stays fully inside the viewport.
|
|
18
|
+
*
|
|
19
|
+
* @param position - Requested top-left coordinates.
|
|
20
|
+
* @param size - Measured menu width and height.
|
|
21
|
+
*/
|
|
22
|
+
function clampMenuPosition(position, size) {
|
|
23
|
+
const margin = 8;
|
|
24
|
+
const maxX = Math.max(margin, window.innerWidth - size.width - margin);
|
|
25
|
+
const maxY = Math.max(margin, window.innerHeight - size.height - margin);
|
|
26
|
+
return {
|
|
27
|
+
x: Math.min(Math.max(position.x, margin), maxX),
|
|
28
|
+
y: Math.min(Math.max(position.y, margin), maxY)
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Cursor-positioned context menu for tab bars, rendered in a portal at the click point.
|
|
33
|
+
*/
|
|
34
|
+
export function TabContextMenu({ groups, position, onClose }) {
|
|
35
|
+
const menuRef = useRef(null);
|
|
36
|
+
const itemRefs = useRef([]);
|
|
37
|
+
const [focusedIndex, setFocusedIndex] = useState(0);
|
|
38
|
+
const [clampedPosition, setClampedPosition] = useState(position);
|
|
39
|
+
const flatItems = useMemo(() => groups.flat(), [groups]);
|
|
40
|
+
/**
|
|
41
|
+
* Closes the menu and notifies the parent.
|
|
42
|
+
*/
|
|
43
|
+
const closeMenu = useCallback(() => {
|
|
44
|
+
onClose();
|
|
45
|
+
}, [onClose]);
|
|
46
|
+
/**
|
|
47
|
+
* Focuses a menu item by index and updates roving tabindex state.
|
|
48
|
+
*
|
|
49
|
+
* @param index - Flat index into `flatItems`.
|
|
50
|
+
*/
|
|
51
|
+
const focusItem = useCallback((index) => {
|
|
52
|
+
setFocusedIndex(index);
|
|
53
|
+
requestAnimationFrame(() => {
|
|
54
|
+
itemRefs.current[index]?.focus();
|
|
55
|
+
});
|
|
56
|
+
}, []);
|
|
57
|
+
/**
|
|
58
|
+
* Re-clamps the menu after mount and focuses the first item once dimensions are known.
|
|
59
|
+
*/
|
|
60
|
+
useLayoutEffect(() => {
|
|
61
|
+
const menu = menuRef.current;
|
|
62
|
+
if (!menu) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const rect = menu.getBoundingClientRect();
|
|
66
|
+
setClampedPosition(clampMenuPosition(position, {
|
|
67
|
+
width: rect.width,
|
|
68
|
+
height: rect.height
|
|
69
|
+
}));
|
|
70
|
+
itemRefs.current[0]?.focus();
|
|
71
|
+
}, [position, groups]);
|
|
72
|
+
/**
|
|
73
|
+
* Closes the menu on outside click or Escape while it is open.
|
|
74
|
+
*/
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
const handleMouseDown = (event) => {
|
|
77
|
+
if (menuRef.current && !menuRef.current.contains(event.target)) {
|
|
78
|
+
closeMenu();
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
const handleKeyDown = (event) => {
|
|
82
|
+
if (event.key === 'Escape') {
|
|
83
|
+
event.preventDefault();
|
|
84
|
+
closeMenu();
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
document.addEventListener('mousedown', handleMouseDown);
|
|
88
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
89
|
+
return () => {
|
|
90
|
+
document.removeEventListener('mousedown', handleMouseDown);
|
|
91
|
+
document.removeEventListener('keydown', handleKeyDown);
|
|
92
|
+
};
|
|
93
|
+
}, [closeMenu]);
|
|
94
|
+
/**
|
|
95
|
+
* Handles keyboard navigation within the open menu.
|
|
96
|
+
*
|
|
97
|
+
* @param event - Keyboard event from the menu panel.
|
|
98
|
+
*/
|
|
99
|
+
const handleMenuKeyDown = (event) => {
|
|
100
|
+
if (flatItems.length === 0) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (event.key === 'Tab') {
|
|
104
|
+
closeMenu();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const arrowIndex = resolveTabListKeyAction(event.key, focusedIndex, flatItems.length);
|
|
108
|
+
if (arrowIndex !== null) {
|
|
109
|
+
event.preventDefault();
|
|
110
|
+
focusItem(arrowIndex);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
if (flatItems.length === 0) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
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) => {
|
|
117
|
+
let flatIndex = groups
|
|
118
|
+
.slice(0, groupIndex)
|
|
119
|
+
.reduce((count, groupItems) => count + groupItems.length, 0);
|
|
120
|
+
return (_jsx("div", { className: groupIndex > 0
|
|
121
|
+
? 'hc-tab-context-menu-group border-t border-separator'
|
|
122
|
+
: 'hc-tab-context-menu-group', children: group.map((item) => {
|
|
123
|
+
const itemIndex = flatIndex++;
|
|
124
|
+
return (_jsx("button", { ref: (element) => {
|
|
125
|
+
itemRefs.current[itemIndex] = element;
|
|
126
|
+
}, type: "button", role: "menuitem", tabIndex: itemIndex === focusedIndex ? 0 : -1, className: cn('hc-tab-context-menu-item', menuItemClass(item.variant)), onClick: (event) => {
|
|
127
|
+
event.stopPropagation();
|
|
128
|
+
closeMenu();
|
|
129
|
+
item.onSelect();
|
|
130
|
+
}, children: item.label }, item.label));
|
|
131
|
+
}) }, groupIndex));
|
|
132
|
+
}) }));
|
|
133
|
+
}
|
|
@@ -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"}
|