@sqlrooms/ui 0.27.0-rc.2 → 0.27.0-rc.4
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/editable-text.d.ts +4 -0
- package/dist/components/editable-text.d.ts.map +1 -1
- package/dist/components/editable-text.js +26 -4
- package/dist/components/editable-text.js.map +1 -1
- package/dist/components/scrollable-row.d.ts +14 -0
- package/dist/components/scrollable-row.d.ts.map +1 -0
- package/dist/components/scrollable-row.js +45 -0
- package/dist/components/scrollable-row.js.map +1 -0
- package/dist/components/tab-strip.d.ts +13 -3
- package/dist/components/tab-strip.d.ts.map +1 -1
- package/dist/components/tab-strip.js +116 -35
- package/dist/components/tab-strip.js.map +1 -1
- package/dist/components/textarea.d.ts.map +1 -1
- package/dist/components/textarea.js +12 -1
- package/dist/components/textarea.js.map +1 -1
- package/dist/components/toast.js +2 -2
- package/dist/components/toast.js.map +1 -1
- package/dist/components/toaster.js +1 -1
- package/dist/components/toaster.js.map +1 -1
- package/dist/components/tree.d.ts.map +1 -1
- package/dist/components/tree.js +7 -6
- package/dist/components/tree.js.map +1 -1
- package/dist/hooks/use-toast.js +1 -1
- package/dist/hooks/use-toast.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/theme/theme-provider.d.ts.map +1 -1
- package/dist/theme/theme-provider.js +25 -4
- package/dist/theme/theme-provider.js.map +1 -1
- package/package.json +2 -2
|
@@ -35,6 +35,10 @@ export declare const EditableText: FC<{
|
|
|
35
35
|
value: string;
|
|
36
36
|
placeholder?: string;
|
|
37
37
|
onChange: (text: string) => void;
|
|
38
|
+
autoFocus?: boolean;
|
|
39
|
+
selectOnFocus?: boolean;
|
|
40
|
+
/** When false, the input is removed from tab order while not editing. */
|
|
41
|
+
allowTabFocusWhenNotEditing?: boolean;
|
|
38
42
|
/**
|
|
39
43
|
* The editing state when it is initially rendered. Use when you do not need to control its editing state
|
|
40
44
|
* in the parent component.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"editable-text.d.ts","sourceRoot":"","sources":["../../src/components/editable-text.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAc,EAAE,EAA2C,MAAM,OAAO,CAAC;AAKhF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,eAAO,MAAM,YAAY,EAAE,EAAE,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"editable-text.d.ts","sourceRoot":"","sources":["../../src/components/editable-text.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAc,EAAE,EAA2C,MAAM,OAAO,CAAC;AAKhF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,eAAO,MAAM,YAAY,EAAE,EAAE,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,yEAAyE;IACzE,2BAA2B,CAAC,EAAE,OAAO,CAAC;IAEtC;;;QAGI;IACJ,cAAc,CAAC,EAAE,OAAO,CAAC;IAEzB;;QAEI;IACJ,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,eAAe,CAAC,EAAE,CAAC,SAAS,EAAE,OAAO,KAAK,IAAI,CAAC;CAChD,CA6JA,CAAC"}
|
|
@@ -33,7 +33,7 @@ import { cn } from '../lib/utils';
|
|
|
33
33
|
* />
|
|
34
34
|
* ```
|
|
35
35
|
*/
|
|
36
|
-
export const EditableText = ({ className, isReadOnly = false, defaultEditing = false, isEditing, placeholder, value, onChange, onEditingChange, }) => {
|
|
36
|
+
export const EditableText = ({ className, isReadOnly = false, defaultEditing = false, isEditing, placeholder, value, onChange, onEditingChange, autoFocus, selectOnFocus = false, allowTabFocusWhenNotEditing = true, }) => {
|
|
37
37
|
const [isInternalEditing, setInternalIsEditing] = useState(defaultEditing);
|
|
38
38
|
const inputRef = useRef(null);
|
|
39
39
|
const [internalValue, setInternalValue] = useState(value);
|
|
@@ -55,10 +55,12 @@ export const EditableText = ({ className, isReadOnly = false, defaultEditing = f
|
|
|
55
55
|
setInternalIsEditing(Boolean(isEditing));
|
|
56
56
|
if (isEditing) {
|
|
57
57
|
// When enabling editing from a dropdown menu, there will be a blur event when the dropdown closes,
|
|
58
|
-
// so we need to wait a bit before making sure the input is focused and selected
|
|
58
|
+
// so we need to wait a bit before making sure the input is focused and selected.
|
|
59
59
|
const timeoutId = setTimeout(() => {
|
|
60
|
-
inputRef.current?.select();
|
|
61
60
|
inputRef.current?.focus();
|
|
61
|
+
if (selectOnFocus) {
|
|
62
|
+
inputRef.current?.select();
|
|
63
|
+
}
|
|
62
64
|
}, 200);
|
|
63
65
|
return () => clearTimeout(timeoutId);
|
|
64
66
|
}
|
|
@@ -88,6 +90,26 @@ export const EditableText = ({ className, isReadOnly = false, defaultEditing = f
|
|
|
88
90
|
handleSetEditing(true);
|
|
89
91
|
}
|
|
90
92
|
}, [isInternalEditing, handleSetEditing]);
|
|
93
|
+
const handleFocus = useCallback(() => {
|
|
94
|
+
if (selectOnFocus && isInternalEditing) {
|
|
95
|
+
inputRef.current?.select();
|
|
96
|
+
}
|
|
97
|
+
}, [isInternalEditing, selectOnFocus]);
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
if (!isInternalEditing)
|
|
100
|
+
return;
|
|
101
|
+
let rafId = requestAnimationFrame(() => {
|
|
102
|
+
rafId = requestAnimationFrame(() => {
|
|
103
|
+
inputRef.current?.focus();
|
|
104
|
+
if (selectOnFocus) {
|
|
105
|
+
inputRef.current?.select();
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
return () => {
|
|
110
|
+
cancelAnimationFrame(rafId);
|
|
111
|
+
};
|
|
112
|
+
}, [isInternalEditing, selectOnFocus]);
|
|
91
113
|
// Add keydown event listener to handle enter key
|
|
92
114
|
useEffect(() => {
|
|
93
115
|
const handleKeyDown = (e) => {
|
|
@@ -118,6 +140,6 @@ export const EditableText = ({ className, isReadOnly = false, defaultEditing = f
|
|
|
118
140
|
truncate: !isInternalEditing,
|
|
119
141
|
}, className), style: {
|
|
120
142
|
caretColor: isInternalEditing ? undefined : 'transparent',
|
|
121
|
-
}, value: internalValue, onChange: handleSetValue, onBlur: handleBlur, disabled: isReadOnly, onClick: handleClick, placeholder: !isInternalEditing ? (placeholder ?? 'Click to edit') : undefined }));
|
|
143
|
+
}, value: internalValue, tabIndex: isInternalEditing || allowTabFocusWhenNotEditing ? 0 : -1, autoFocus: autoFocus, onChange: handleSetValue, onFocus: handleFocus, onBlur: handleBlur, disabled: isReadOnly, onClick: handleClick, placeholder: !isInternalEditing ? (placeholder ?? 'Click to edit') : undefined }));
|
|
122
144
|
};
|
|
123
145
|
//# sourceMappingURL=editable-text.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"editable-text.js","sourceRoot":"","sources":["../../src/components/editable-text.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport {ChangeEvent, FC, useCallback, useEffect, useRef, useState} from 'react';\n\nimport {Input} from './input';\nimport {cn} from '../lib/utils';\n\n/**\n * Component that allows the user to edit a string.\n *\n * The editing mode can be controlled (the mode is managed by the parent component)\n * or uncontrolled (managed by the component itself).\n *\n * Controlled mode example:\n * ```\n * const [text, setText] = useState('');\n * const [isEditing, setEditing] = useState(false);\n * ...\n * <EditableText\n * value={text}\n * onChange={setText}\n * isEditing={isEditing}\n * onEditingChange={setEditing}\n * />\n * ```\n *\n * Uncontrolled mode example:\n * ```\n * const [text, setText] = useState('');\n * ...\n * <EditableText\n * value={text}\n * onChange={setText}\n * defaultEditing={false}\n * />\n * ```\n */\n\nexport const EditableText: FC<{\n className?: string;\n isReadOnly?: boolean;\n value: string;\n placeholder?: string;\n onChange: (text: string) => void;\n\n /**\n * The editing state when it is initially rendered. Use when you do not need to control its editing state\n * in the parent component.\n **/\n defaultEditing?: boolean;\n\n /**\n * The controlled editing state of the component. Must be used in conjunction with onEditingChange.\n **/\n isEditing?: boolean;\n onEditingChange?: (isEditing: boolean) => void;\n}> = ({\n className,\n isReadOnly = false,\n defaultEditing = false,\n isEditing,\n placeholder,\n value,\n onChange,\n onEditingChange,\n}) => {\n const [isInternalEditing, setInternalIsEditing] = useState(defaultEditing);\n const inputRef = useRef<HTMLInputElement>(null);\n const [internalValue, setInternalValue] = useState(value);\n const internalValueRef = useRef(internalValue);\n\n useEffect(() => {\n internalValueRef.current = internalValue;\n }, [internalValue]);\n\n // Keep internalValue in sync with value prop\n /* eslint-disable react-hooks/set-state-in-effect */\n useEffect(() => {\n if (value !== internalValueRef.current) {\n setInternalValue(value);\n }\n }, [value]);\n\n // Keep internal editing state in sync with controlled isEditing prop\n // and focus the input when editing is enabled\n useEffect(() => {\n if (isEditing !== undefined && isEditing !== isInternalEditing) {\n setInternalIsEditing(Boolean(isEditing));\n if (isEditing) {\n // When enabling editing from a dropdown menu, there will be a blur event when the dropdown closes,\n // so we need to wait a bit before making sure the input is focused and selected\n const timeoutId = setTimeout(() => {\n inputRef.current?.select();\n inputRef.current?.focus();\n }, 200);\n return () => clearTimeout(timeoutId);\n }\n }\n return undefined;\n }, [isEditing, isInternalEditing]);\n /* eslint-enable react-hooks/set-state-in-effect */\n\n const handleSetValue = useCallback(\n (e: ChangeEvent<HTMLInputElement>) => {\n if (isReadOnly || !isInternalEditing) {\n return;\n }\n return setInternalValue(e.target.value);\n },\n [isInternalEditing, isReadOnly],\n );\n\n const handleSetEditing = useCallback(\n (nextIsEditing: boolean) => {\n if (isReadOnly) {\n return;\n }\n setInternalIsEditing(nextIsEditing);\n onEditingChange?.(nextIsEditing);\n },\n [isReadOnly, onEditingChange],\n );\n\n const handleBlur = useCallback(() => {\n handleSetEditing(false);\n onChange(internalValueRef.current?.trim());\n }, [handleSetEditing, onChange]);\n\n const handleClick = useCallback(() => {\n if (!isInternalEditing) {\n handleSetEditing(true);\n }\n }, [isInternalEditing, handleSetEditing]);\n\n // Add keydown event listener to handle enter key\n useEffect(() => {\n const handleKeyDown = (e: KeyboardEvent) => {\n switch (e.key) {\n case 'Escape':\n // Reset the internal value to the original value\n setInternalValue(value);\n internalValueRef.current = value;\n handleSetEditing(false);\n inputRef.current?.blur();\n break;\n case 'Enter':\n handleSetEditing(false);\n onChange(internalValueRef.current.trim());\n inputRef.current?.blur();\n break;\n }\n };\n if (isInternalEditing) {\n document.addEventListener('keydown', handleKeyDown);\n }\n return () => {\n document.removeEventListener('keydown', handleKeyDown);\n };\n }, [isInternalEditing, onChange, handleSetEditing, value]);\n\n return (\n <Input\n ref={inputRef}\n className={cn(\n 'disabled:opacity-1 w-full rounded-sm border-transparent px-1 py-0 focus:border-blue-500 focus:outline-none focus:ring-blue-500 disabled:cursor-text',\n {\n 'select-none bg-transparent': !isInternalEditing,\n truncate: !isInternalEditing,\n },\n className,\n )}\n style={{\n caretColor: isInternalEditing ? undefined : 'transparent',\n }}\n value={internalValue}\n onChange={handleSetValue}\n onBlur={handleBlur}\n disabled={isReadOnly}\n onClick={handleClick}\n placeholder={\n !isInternalEditing ? (placeholder ?? 'Click to edit') : undefined\n }\n />\n );\n};\n"]}
|
|
1
|
+
{"version":3,"file":"editable-text.js","sourceRoot":"","sources":["../../src/components/editable-text.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport {ChangeEvent, FC, useCallback, useEffect, useRef, useState} from 'react';\n\nimport {Input} from './input';\nimport {cn} from '../lib/utils';\n\n/**\n * Component that allows the user to edit a string.\n *\n * The editing mode can be controlled (the mode is managed by the parent component)\n * or uncontrolled (managed by the component itself).\n *\n * Controlled mode example:\n * ```\n * const [text, setText] = useState('');\n * const [isEditing, setEditing] = useState(false);\n * ...\n * <EditableText\n * value={text}\n * onChange={setText}\n * isEditing={isEditing}\n * onEditingChange={setEditing}\n * />\n * ```\n *\n * Uncontrolled mode example:\n * ```\n * const [text, setText] = useState('');\n * ...\n * <EditableText\n * value={text}\n * onChange={setText}\n * defaultEditing={false}\n * />\n * ```\n */\n\nexport const EditableText: FC<{\n className?: string;\n isReadOnly?: boolean;\n value: string;\n placeholder?: string;\n onChange: (text: string) => void;\n autoFocus?: boolean;\n selectOnFocus?: boolean;\n /** When false, the input is removed from tab order while not editing. */\n allowTabFocusWhenNotEditing?: boolean;\n\n /**\n * The editing state when it is initially rendered. Use when you do not need to control its editing state\n * in the parent component.\n **/\n defaultEditing?: boolean;\n\n /**\n * The controlled editing state of the component. Must be used in conjunction with onEditingChange.\n **/\n isEditing?: boolean;\n onEditingChange?: (isEditing: boolean) => void;\n}> = ({\n className,\n isReadOnly = false,\n defaultEditing = false,\n isEditing,\n placeholder,\n value,\n onChange,\n onEditingChange,\n autoFocus,\n selectOnFocus = false,\n allowTabFocusWhenNotEditing = true,\n}) => {\n const [isInternalEditing, setInternalIsEditing] = useState(defaultEditing);\n const inputRef = useRef<HTMLInputElement>(null);\n const [internalValue, setInternalValue] = useState(value);\n const internalValueRef = useRef(internalValue);\n\n useEffect(() => {\n internalValueRef.current = internalValue;\n }, [internalValue]);\n\n // Keep internalValue in sync with value prop\n /* eslint-disable react-hooks/set-state-in-effect */\n useEffect(() => {\n if (value !== internalValueRef.current) {\n setInternalValue(value);\n }\n }, [value]);\n\n // Keep internal editing state in sync with controlled isEditing prop\n // and focus the input when editing is enabled\n useEffect(() => {\n if (isEditing !== undefined && isEditing !== isInternalEditing) {\n setInternalIsEditing(Boolean(isEditing));\n if (isEditing) {\n // When enabling editing from a dropdown menu, there will be a blur event when the dropdown closes,\n // so we need to wait a bit before making sure the input is focused and selected.\n const timeoutId = setTimeout(() => {\n inputRef.current?.focus();\n if (selectOnFocus) {\n inputRef.current?.select();\n }\n }, 200);\n return () => clearTimeout(timeoutId);\n }\n }\n return undefined;\n }, [isEditing, isInternalEditing]);\n /* eslint-enable react-hooks/set-state-in-effect */\n\n const handleSetValue = useCallback(\n (e: ChangeEvent<HTMLInputElement>) => {\n if (isReadOnly || !isInternalEditing) {\n return;\n }\n return setInternalValue(e.target.value);\n },\n [isInternalEditing, isReadOnly],\n );\n\n const handleSetEditing = useCallback(\n (nextIsEditing: boolean) => {\n if (isReadOnly) {\n return;\n }\n setInternalIsEditing(nextIsEditing);\n onEditingChange?.(nextIsEditing);\n },\n [isReadOnly, onEditingChange],\n );\n\n const handleBlur = useCallback(() => {\n handleSetEditing(false);\n onChange(internalValueRef.current?.trim());\n }, [handleSetEditing, onChange]);\n\n const handleClick = useCallback(() => {\n if (!isInternalEditing) {\n handleSetEditing(true);\n }\n }, [isInternalEditing, handleSetEditing]);\n\n const handleFocus = useCallback(() => {\n if (selectOnFocus && isInternalEditing) {\n inputRef.current?.select();\n }\n }, [isInternalEditing, selectOnFocus]);\n\n useEffect(() => {\n if (!isInternalEditing) return;\n let rafId = requestAnimationFrame(() => {\n rafId = requestAnimationFrame(() => {\n inputRef.current?.focus();\n if (selectOnFocus) {\n inputRef.current?.select();\n }\n });\n });\n return () => {\n cancelAnimationFrame(rafId);\n };\n }, [isInternalEditing, selectOnFocus]);\n\n // Add keydown event listener to handle enter key\n useEffect(() => {\n const handleKeyDown = (e: KeyboardEvent) => {\n switch (e.key) {\n case 'Escape':\n // Reset the internal value to the original value\n setInternalValue(value);\n internalValueRef.current = value;\n handleSetEditing(false);\n inputRef.current?.blur();\n break;\n case 'Enter':\n handleSetEditing(false);\n onChange(internalValueRef.current.trim());\n inputRef.current?.blur();\n break;\n }\n };\n if (isInternalEditing) {\n document.addEventListener('keydown', handleKeyDown);\n }\n return () => {\n document.removeEventListener('keydown', handleKeyDown);\n };\n }, [isInternalEditing, onChange, handleSetEditing, value]);\n\n return (\n <Input\n ref={inputRef}\n className={cn(\n 'disabled:opacity-1 w-full rounded-sm border-transparent px-1 py-0 focus:border-blue-500 focus:outline-none focus:ring-blue-500 disabled:cursor-text',\n {\n 'select-none bg-transparent': !isInternalEditing,\n truncate: !isInternalEditing,\n },\n className,\n )}\n style={{\n caretColor: isInternalEditing ? undefined : 'transparent',\n }}\n value={internalValue}\n tabIndex={isInternalEditing || allowTabFocusWhenNotEditing ? 0 : -1}\n autoFocus={autoFocus}\n onChange={handleSetValue}\n onFocus={handleFocus}\n onBlur={handleBlur}\n disabled={isReadOnly}\n onClick={handleClick}\n placeholder={\n !isInternalEditing ? (placeholder ?? 'Click to edit') : undefined\n }\n />\n );\n};\n"]}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
type ScrollableRowProps = {
|
|
3
|
+
children: React.ReactNode;
|
|
4
|
+
className?: string;
|
|
5
|
+
scrollClassName?: string;
|
|
6
|
+
scrollRef?: React.RefObject<HTMLDivElement>;
|
|
7
|
+
scrollAmount?: number;
|
|
8
|
+
arrowVisibility?: 'hover' | 'always';
|
|
9
|
+
arrowClassName?: string;
|
|
10
|
+
arrowIconClassName?: string;
|
|
11
|
+
};
|
|
12
|
+
export declare function ScrollableRow({ children, className, scrollClassName, scrollRef, scrollAmount, arrowVisibility, arrowClassName, arrowIconClassName, }: ScrollableRowProps): import("react/jsx-runtime").JSX.Element;
|
|
13
|
+
export {};
|
|
14
|
+
//# sourceMappingURL=scrollable-row.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scrollable-row.d.ts","sourceRoot":"","sources":["../../src/components/scrollable-row.tsx"],"names":[],"mappings":"AACA,OAAO,KAAoC,MAAM,OAAO,CAAC;AAGzD,KAAK,kBAAkB,GAAG;IACxB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,SAAS,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC;IAC5C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,eAAe,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC;IACrC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B,CAAC;AAEF,wBAAgB,aAAa,CAAC,EAC5B,QAAQ,EACR,SAAS,EACT,eAAe,EACf,SAAS,EACT,YAAkB,EAClB,eAAyB,EACzB,cAAc,EACd,kBAAkB,GACnB,EAAE,kBAAkB,2CAkGpB"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
|
3
|
+
import { useEffect, useRef, useState } from 'react';
|
|
4
|
+
import { cn } from '../lib/utils';
|
|
5
|
+
export function ScrollableRow({ children, className, scrollClassName, scrollRef, scrollAmount = 200, arrowVisibility = 'hover', arrowClassName, arrowIconClassName, }) {
|
|
6
|
+
const internalRef = useRef(null);
|
|
7
|
+
const containerRef = scrollRef ?? internalRef;
|
|
8
|
+
const [canScrollLeft, setCanScrollLeft] = useState(false);
|
|
9
|
+
const [canScrollRight, setCanScrollRight] = useState(false);
|
|
10
|
+
const updateScrollState = () => {
|
|
11
|
+
const container = containerRef.current;
|
|
12
|
+
if (!container)
|
|
13
|
+
return;
|
|
14
|
+
const { scrollLeft, scrollWidth, clientWidth } = container;
|
|
15
|
+
setCanScrollLeft(scrollLeft > 0);
|
|
16
|
+
setCanScrollRight(scrollLeft + clientWidth < scrollWidth - 1);
|
|
17
|
+
};
|
|
18
|
+
const scrollBy = (direction) => {
|
|
19
|
+
const container = containerRef.current;
|
|
20
|
+
if (!container)
|
|
21
|
+
return;
|
|
22
|
+
container.scrollBy({
|
|
23
|
+
left: direction === 'left' ? -scrollAmount : scrollAmount,
|
|
24
|
+
behavior: 'smooth',
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
const container = containerRef.current;
|
|
29
|
+
if (!container)
|
|
30
|
+
return;
|
|
31
|
+
updateScrollState();
|
|
32
|
+
container.addEventListener('scroll', updateScrollState);
|
|
33
|
+
const resizeObserver = new ResizeObserver(updateScrollState);
|
|
34
|
+
resizeObserver.observe(container);
|
|
35
|
+
return () => {
|
|
36
|
+
container.removeEventListener('scroll', updateScrollState);
|
|
37
|
+
resizeObserver.disconnect();
|
|
38
|
+
};
|
|
39
|
+
}, [children, containerRef]);
|
|
40
|
+
const arrowBaseClass = cn('absolute top-0 z-10 flex h-full w-8 items-center backdrop-blur-md bg-background/50 transition-colors', arrowVisibility === 'hover'
|
|
41
|
+
? 'opacity-0 transition-opacity hover:opacity-100'
|
|
42
|
+
: 'opacity-100', arrowClassName);
|
|
43
|
+
return (_jsxs("div", { className: cn('relative', className), children: [_jsx("button", { type: "button", onClick: () => scrollBy('left'), disabled: !canScrollLeft, className: cn(arrowBaseClass, 'left-0 justify-start pl-1', 'from-background/90 via-background/60 group bg-gradient-to-r to-transparent', !canScrollLeft && 'pointer-events-none opacity-0'), "aria-label": "Scroll left", title: "Scroll left", children: _jsx(ChevronLeft, { className: cn('text-muted-foreground group-hover:text-foreground h-5 w-5 transition-colors', arrowIconClassName) }) }), _jsx("div", { ref: containerRef, className: scrollClassName, children: children }), _jsx("button", { type: "button", onClick: () => scrollBy('right'), disabled: !canScrollRight, className: cn(arrowBaseClass, 'right-0 justify-end pr-1', 'from-background/90 via-background/60 group bg-gradient-to-l to-transparent', !canScrollRight && 'pointer-events-none opacity-0'), "aria-label": "Scroll right", title: "Scroll right", children: _jsx(ChevronRight, { className: cn('text-muted-foreground group-hover:text-foreground h-5 w-5 transition-colors', arrowIconClassName) }) })] }));
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=scrollable-row.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scrollable-row.js","sourceRoot":"","sources":["../../src/components/scrollable-row.tsx"],"names":[],"mappings":"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","sourcesContent":["import {ChevronLeft, ChevronRight} from 'lucide-react';\nimport React, {useEffect, useRef, useState} from 'react';\nimport {cn} from '../lib/utils';\n\ntype ScrollableRowProps = {\n children: React.ReactNode;\n className?: string;\n scrollClassName?: string;\n scrollRef?: React.RefObject<HTMLDivElement>;\n scrollAmount?: number;\n arrowVisibility?: 'hover' | 'always';\n arrowClassName?: string;\n arrowIconClassName?: string;\n};\n\nexport function ScrollableRow({\n children,\n className,\n scrollClassName,\n scrollRef,\n scrollAmount = 200,\n arrowVisibility = 'hover',\n arrowClassName,\n arrowIconClassName,\n}: ScrollableRowProps) {\n const internalRef = useRef<HTMLDivElement>(null);\n const containerRef = scrollRef ?? internalRef;\n const [canScrollLeft, setCanScrollLeft] = useState(false);\n const [canScrollRight, setCanScrollRight] = useState(false);\n\n const updateScrollState = () => {\n const container = containerRef.current;\n if (!container) return;\n\n const {scrollLeft, scrollWidth, clientWidth} = container;\n setCanScrollLeft(scrollLeft > 0);\n setCanScrollRight(scrollLeft + clientWidth < scrollWidth - 1);\n };\n\n const scrollBy = (direction: 'left' | 'right') => {\n const container = containerRef.current;\n if (!container) return;\n\n container.scrollBy({\n left: direction === 'left' ? -scrollAmount : scrollAmount,\n behavior: 'smooth',\n });\n };\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n updateScrollState();\n\n container.addEventListener('scroll', updateScrollState);\n const resizeObserver = new ResizeObserver(updateScrollState);\n resizeObserver.observe(container);\n\n return () => {\n container.removeEventListener('scroll', updateScrollState);\n resizeObserver.disconnect();\n };\n }, [children, containerRef]);\n\n const arrowBaseClass = cn(\n 'absolute top-0 z-10 flex h-full w-8 items-center backdrop-blur-md bg-background/50 transition-colors',\n arrowVisibility === 'hover'\n ? 'opacity-0 transition-opacity hover:opacity-100'\n : 'opacity-100',\n arrowClassName,\n );\n\n return (\n <div className={cn('relative', className)}>\n <button\n type=\"button\"\n onClick={() => scrollBy('left')}\n disabled={!canScrollLeft}\n className={cn(\n arrowBaseClass,\n 'left-0 justify-start pl-1',\n 'from-background/90 via-background/60 group bg-gradient-to-r to-transparent',\n !canScrollLeft && 'pointer-events-none opacity-0',\n )}\n aria-label=\"Scroll left\"\n title=\"Scroll left\"\n >\n <ChevronLeft\n className={cn(\n 'text-muted-foreground group-hover:text-foreground h-5 w-5 transition-colors',\n arrowIconClassName,\n )}\n />\n </button>\n\n <div ref={containerRef} className={scrollClassName}>\n {children}\n </div>\n\n <button\n type=\"button\"\n onClick={() => scrollBy('right')}\n disabled={!canScrollRight}\n className={cn(\n arrowBaseClass,\n 'right-0 justify-end pr-1',\n 'from-background/90 via-background/60 group bg-gradient-to-l to-transparent',\n !canScrollRight && 'pointer-events-none opacity-0',\n )}\n aria-label=\"Scroll right\"\n title=\"Scroll right\"\n >\n <ChevronRight\n className={cn(\n 'text-muted-foreground group-hover:text-foreground h-5 w-5 transition-colors',\n arrowIconClassName,\n )}\n />\n </button>\n </div>\n );\n}\n"]}
|
|
@@ -50,12 +50,22 @@ interface TabStripSearchDropdownProps {
|
|
|
50
50
|
tooltip?: React.ReactNode;
|
|
51
51
|
/** Optional custom icon for the trigger button. */
|
|
52
52
|
triggerIcon?: React.ReactNode;
|
|
53
|
+
/** Message shown when there are no tabs at all. */
|
|
54
|
+
emptyMessage?: React.ReactNode;
|
|
55
|
+
/** Message shown when searching and there are no matching tabs. */
|
|
56
|
+
searchEmptyMessage?: React.ReactNode;
|
|
57
|
+
/** Label for the closed tabs group. */
|
|
58
|
+
closedTabsLabel?: React.ReactNode;
|
|
59
|
+
/** Sorting mode for search dropdown items. */
|
|
60
|
+
sortSearchItems?: 'none' | 'recent';
|
|
61
|
+
/** Optional accessor for tab recency timestamps. */
|
|
62
|
+
getTabLastOpenedAt?: (tab: TabDescriptor) => number | undefined;
|
|
53
63
|
}
|
|
54
64
|
/**
|
|
55
65
|
* Renders the dropdown with search for browsing tabs.
|
|
56
|
-
*
|
|
66
|
+
* Shows open tabs first and closed tabs second (dimmed). When searching, shows all matching tabs.
|
|
57
67
|
*/
|
|
58
|
-
declare function TabStripSearchDropdown({ className, triggerClassName, autoFocus, tooltip, triggerIcon, }: TabStripSearchDropdownProps): import("react/jsx-runtime").JSX.Element;
|
|
68
|
+
declare function TabStripSearchDropdown({ className, triggerClassName, autoFocus, tooltip, triggerIcon, emptyMessage, searchEmptyMessage, closedTabsLabel, sortSearchItems, getTabLastOpenedAt, }: TabStripSearchDropdownProps): import("react/jsx-runtime").JSX.Element;
|
|
59
69
|
interface TabStripNewButtonProps {
|
|
60
70
|
className?: string;
|
|
61
71
|
/** Optional tooltip content for the button. */
|
|
@@ -72,7 +82,7 @@ export interface TabStripProps {
|
|
|
72
82
|
/** All available tabs. */
|
|
73
83
|
tabs: TabDescriptor[];
|
|
74
84
|
/** IDs of tabs that are currently open. */
|
|
75
|
-
openTabs
|
|
85
|
+
openTabs?: string[];
|
|
76
86
|
/** ID of the currently selected tab. */
|
|
77
87
|
selectedTabId?: string | null;
|
|
78
88
|
/** If true, hides the close button when only one tab remains open. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tab-strip.d.ts","sourceRoot":"","sources":["../../src/components/tab-strip.tsx"],"names":[],"mappings":"AAyBA,OAAO,
|
|
1
|
+
{"version":3,"file":"tab-strip.d.ts","sourceRoot":"","sources":["../../src/components/tab-strip.tsx"],"names":[],"mappings":"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"}
|
|
@@ -3,13 +3,14 @@ import { DndContext, PointerSensor, closestCenter, useSensor, useSensors, } from
|
|
|
3
3
|
import { restrictToHorizontalAxis, restrictToParentElement, } from '@dnd-kit/modifiers';
|
|
4
4
|
import { SortableContext, arrayMove, horizontalListSortingStrategy, useSortable, } from '@dnd-kit/sortable';
|
|
5
5
|
import { EllipsisVerticalIcon, ListCollapseIcon, PlusIcon, SearchIcon, XIcon, } from 'lucide-react';
|
|
6
|
-
import { createContext, useContext, useEffect, useMemo, useRef, useState, } from 'react';
|
|
6
|
+
import { createContext, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, } from 'react';
|
|
7
7
|
const DRAG_MODIFIERS = [restrictToHorizontalAxis, restrictToParentElement];
|
|
8
8
|
import { cn } from '../lib/utils';
|
|
9
9
|
import { Button } from './button';
|
|
10
10
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from './dropdown-menu';
|
|
11
11
|
import { EditableText } from './editable-text';
|
|
12
12
|
import { Input } from './input';
|
|
13
|
+
import { ScrollableRow } from './scrollable-row';
|
|
13
14
|
import { Tabs, TabsList, TabsTrigger } from './tabs';
|
|
14
15
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from './tooltip';
|
|
15
16
|
const TabStripContext = createContext(null);
|
|
@@ -34,23 +35,23 @@ function SortableTab({ tab, tabClassName, editingTabId, hideCloseButton, onClose
|
|
|
34
35
|
opacity: isDragging ? 0.5 : 1,
|
|
35
36
|
};
|
|
36
37
|
const menuContent = renderTabMenu?.(tab);
|
|
37
|
-
return (_jsx("div", { ref: setNodeRef, className: "h-full flex-shrink-0", style: style, ...attributes, ...listeners, children:
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
38
|
+
return (_jsx("div", { ref: setNodeRef, className: "h-full flex-shrink-0", style: style, "data-tab-id": tab.id, ...attributes, ...listeners, tabIndex: -1, children: _jsx("div", { "data-state": editingTabId === tab.id ? 'editing' : undefined, className: cn('data-[state=inactive]:hover:bg-primary/5', 'group flex h-full min-w-[100px] max-w-[200px] flex-shrink-0 cursor-grab', 'items-center gap-0.5 overflow-visible rounded-b-none', 'py-0 pl-0 pr-0 font-normal data-[state=active]:shadow-none', tabClassName, editingTabId === tab.id && 'focus-visible:ring-0'), children: _jsxs("div", { className: "relative flex h-full min-w-0 flex-1 items-center", children: [_jsx(TabsTrigger, { value: tab.id, tabIndex: editingTabId === tab.id ? -1 : undefined, "data-editing": editingTabId === tab.id ? '' : undefined, className: cn('flex h-full min-w-0 flex-1 items-center justify-start gap-1', 'hover:bg-primary/10 overflow-hidden px-6 py-1 font-normal', 'min-h-7', 'data-[state=active]:bg-primary/10 data-[state=active]:text-foreground data-[state=active]:shadow-none', 'focus-visible:ring-primary focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-offset-0', editingTabId === tab.id && 'focus-visible:ring-0'), onDoubleClick: () => onStartEditing(tab.id), children: editingTabId !== tab.id ? (_jsx("div", { className: "truncate text-sm", children: renderTabLabel ? renderTabLabel(tab) : tab.name })) : (_jsx(EditableText, { value: tab.name, onChange: (newName) => onInlineRename(tab.id, newName), className: "h-6 min-w-0 flex-1 truncate text-sm shadow-none", isEditing: true, autoFocus: true, selectOnFocus: true, allowTabFocusWhenNotEditing: false, onEditingChange: (isEditing) => {
|
|
39
|
+
if (!isEditing) {
|
|
40
|
+
onStopEditing();
|
|
41
|
+
}
|
|
42
|
+
} })) }), menuContent && (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx("button", { type: "button", "aria-label": "Tab options", className: "hover:bg-primary/10 focus-visible:bg-primary/10 focus-visible:ring-primary absolute left-1 top-1/2 flex h-5 w-5 -translate-y-1/2 items-center justify-center rounded p-1 opacity-0 outline-none focus-visible:opacity-100 focus-visible:ring-2 focus-visible:ring-offset-0 group-hover:opacity-100 data-[state=open]:opacity-100", onMouseDown: (event) => {
|
|
43
|
+
event.stopPropagation();
|
|
44
|
+
event.preventDefault();
|
|
45
|
+
}, onClick: (event) => {
|
|
46
|
+
event.stopPropagation();
|
|
47
|
+
}, children: _jsx(EllipsisVerticalIcon, { className: "h-3 w-3" }) }) }), _jsx(DropdownMenuContent, { align: "start", children: menuContent })] })), !hideCloseButton && (_jsx("button", { type: "button", "aria-label": "Close tab", className: "hover:bg-primary/10 focus-visible:bg-primary/10 focus-visible:ring-primary absolute right-1 top-1/2 flex h-5 w-5 -translate-y-1/2 items-center justify-center rounded p-1 opacity-0 outline-none focus-visible:opacity-100 focus-visible:ring-2 focus-visible:ring-offset-0 group-hover:opacity-100", onMouseDown: (event) => {
|
|
48
|
+
event.stopPropagation();
|
|
49
|
+
event.preventDefault();
|
|
50
|
+
}, onClick: (event) => {
|
|
51
|
+
event.stopPropagation();
|
|
52
|
+
event.preventDefault();
|
|
53
|
+
onClose(tab.id);
|
|
54
|
+
}, children: _jsx(XIcon, { className: "h-4 w-4" }) }))] }) }) }));
|
|
54
55
|
}
|
|
55
56
|
/**
|
|
56
57
|
* A menu item for the tab's dropdown menu.
|
|
@@ -98,14 +99,14 @@ function TabStripTabs({ className, tabClassName }) {
|
|
|
98
99
|
}
|
|
99
100
|
};
|
|
100
101
|
const tabIds = useMemo(() => openTabItems.map((t) => t.id), [openTabItems]);
|
|
101
|
-
return (_jsx(DndContext, { sensors: sensors, collisionDetection: closestCenter, modifiers: DRAG_MODIFIERS, autoScroll: true, onDragEnd: handleDragEnd, children: _jsx(SortableContext, { items: tabIds, strategy: horizontalListSortingStrategy, children: _jsx(
|
|
102
|
+
return (_jsx(DndContext, { sensors: sensors, collisionDetection: closestCenter, modifiers: DRAG_MODIFIERS, autoScroll: true, onDragEnd: handleDragEnd, children: _jsx(SortableContext, { items: tabIds, strategy: horizontalListSortingStrategy, children: _jsx(ScrollableRow, { className: "h-full min-w-0 flex-1", scrollRef: scrollContainerRef, scrollClassName: cn('flex h-full min-w-0 items-center gap-1 overflow-x-auto overflow-y-visible', 'py-1 pl-1 pr-1 scroll-pl-7 scroll-pr-7 [&::-webkit-scrollbar]:hidden', className), arrowVisibility: "always", arrowClassName: "w-7", arrowIconClassName: "h-4 w-4 opacity-60", children: openTabItems.map((tab) => (_jsx(SortableTab, { tab: tab, tabClassName: tabClassName, editingTabId: editingTabId, hideCloseButton: preventCloseLastTab && openTabItems.length === 1, onClose: handleClose, onStartEditing: handleStartEditing, onStopEditing: handleStopEditing, onInlineRename: handleInlineRename, renderTabMenu: renderTabMenu, renderTabLabel: renderTabLabel }, tab.id))) }) }) }));
|
|
102
103
|
}
|
|
103
104
|
/**
|
|
104
105
|
* Renders the dropdown with search for browsing tabs.
|
|
105
|
-
*
|
|
106
|
+
* Shows open tabs first and closed tabs second (dimmed). When searching, shows all matching tabs.
|
|
106
107
|
*/
|
|
107
|
-
function TabStripSearchDropdown({ className, triggerClassName, autoFocus = true, tooltip, triggerIcon, }) {
|
|
108
|
-
const { search, setSearch, closedTabs, filteredTabs, closedTabIds, openTabs, onOpenTabsChange, onSelect, renderSearchItemActions, } = useTabStripContext();
|
|
108
|
+
function TabStripSearchDropdown({ className, triggerClassName, autoFocus = true, tooltip, triggerIcon, emptyMessage = 'No tabs', searchEmptyMessage = 'No matching tabs', closedTabsLabel = 'Recently closed', sortSearchItems = 'recent', getTabLastOpenedAt, }) {
|
|
109
|
+
const { openTabItems, search, setSearch, closedTabs, filteredTabs, closedTabIds, openTabs, preventCloseLastTab, onOpenTabsChange, onSelect, renderSearchItemActions, getLastOpenedAt, handleClose, } = useTabStripContext();
|
|
109
110
|
const [isOpen, setIsOpen] = useState(false);
|
|
110
111
|
const isSearching = search.trim().length > 0;
|
|
111
112
|
const handleOpenChange = (open) => {
|
|
@@ -117,7 +118,7 @@ function TabStripSearchDropdown({ className, triggerClassName, autoFocus = true,
|
|
|
117
118
|
const handleTabClick = (tabId) => {
|
|
118
119
|
if (closedTabIds.has(tabId)) {
|
|
119
120
|
// Opening a closed tab: add to openTabs and select it
|
|
120
|
-
onOpenTabsChange?.([...openTabs, tabId]);
|
|
121
|
+
onOpenTabsChange?.([...(openTabs ?? []), tabId]);
|
|
121
122
|
onSelect?.(tabId);
|
|
122
123
|
}
|
|
123
124
|
else {
|
|
@@ -126,8 +127,38 @@ function TabStripSearchDropdown({ className, triggerClassName, autoFocus = true,
|
|
|
126
127
|
}
|
|
127
128
|
setIsOpen(false);
|
|
128
129
|
};
|
|
130
|
+
const sortByRecency = (tabsToSort) => {
|
|
131
|
+
if (sortSearchItems !== 'recent') {
|
|
132
|
+
return tabsToSort;
|
|
133
|
+
}
|
|
134
|
+
const withIndex = tabsToSort.map((tab, index) => ({
|
|
135
|
+
tab,
|
|
136
|
+
index,
|
|
137
|
+
ts: getTabLastOpenedAt?.(tab) ?? getLastOpenedAt(tab.id),
|
|
138
|
+
}));
|
|
139
|
+
return withIndex
|
|
140
|
+
.slice()
|
|
141
|
+
.sort((a, b) => {
|
|
142
|
+
const aTs = a.ts ?? -1;
|
|
143
|
+
const bTs = b.ts ?? -1;
|
|
144
|
+
if (aTs === bTs) {
|
|
145
|
+
return a.index - b.index;
|
|
146
|
+
}
|
|
147
|
+
return bTs - aTs;
|
|
148
|
+
})
|
|
149
|
+
.map((item) => item.tab);
|
|
150
|
+
};
|
|
151
|
+
const openTabsList = sortByRecency(openTabItems);
|
|
152
|
+
const closedTabsList = sortByRecency(closedTabs);
|
|
153
|
+
const hasAnyTabs = openTabsList.length + closedTabsList.length > 0;
|
|
154
|
+
const filteredOpenTabs = sortByRecency(filteredTabs.filter((tab) => !closedTabIds.has(tab.id)));
|
|
155
|
+
const filteredClosedTabs = sortByRecency(filteredTabs.filter((tab) => closedTabIds.has(tab.id)));
|
|
156
|
+
const renderOpenTabActions = (tab) => {
|
|
157
|
+
const disableClose = preventCloseLastTab && (openTabsList.length ?? 0) <= 1;
|
|
158
|
+
return (_jsxs(_Fragment, { children: [renderSearchItemActions?.(tab), _jsx(TabStripSearchItemAction, { icon: _jsx(XIcon, { className: "h-3 w-3" }), "aria-label": `Close ${tab.name}`, onClick: disableClose ? undefined : () => handleClose(tab.id), className: cn(disableClose && 'pointer-events-none opacity-40') })] }));
|
|
159
|
+
};
|
|
129
160
|
const triggerButton = (_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", "aria-label": "Browse tabs", className: cn('hover:bg-primary/10 h-full flex-shrink-0', triggerClassName), children: triggerIcon ?? _jsx(ListCollapseIcon, { className: "h-4 w-4" }) }) }));
|
|
130
|
-
return (_jsxs(DropdownMenu, { open: isOpen, onOpenChange: handleOpenChange, children: [tooltip ? (_jsx(TooltipProvider, { children: _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: triggerButton }), _jsx(TooltipContent, { children: tooltip })] }) })) : (triggerButton), _jsxs(DropdownMenuContent, { align: "start", onCloseAutoFocus: (event) => event.preventDefault(), className: cn('flex max-h-[400px] max-w-[240px] flex-col', className), children: [_jsxs("div", { className: "flex flex-shrink-0 items-center gap-1 px-2", children: [_jsx(SearchIcon, { className: "text-muted-foreground", size: 14 }), _jsx(Input, { value: search, onChange: (event) => setSearch(event.target.value), onKeyDown: (event) => {
|
|
161
|
+
return (_jsxs(DropdownMenu, { open: isOpen, onOpenChange: handleOpenChange, children: [tooltip ? (_jsx(TooltipProvider, { children: _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: triggerButton }), _jsx(TooltipContent, { children: tooltip })] }) })) : (triggerButton), _jsxs(DropdownMenuContent, { align: "start", onCloseAutoFocus: (event) => event.preventDefault(), className: cn('flex max-h-[400px] max-w-[240px] flex-col overflow-x-hidden', className), children: [_jsxs("div", { className: "flex flex-shrink-0 items-center gap-1 px-2", children: [_jsx(SearchIcon, { className: "text-muted-foreground", size: 14 }), _jsx(Input, { value: search, onChange: (event) => setSearch(event.target.value), onKeyDown: (event) => {
|
|
131
162
|
if (event.key === 'ArrowDown' || event.key === 'Tab') {
|
|
132
163
|
event.preventDefault();
|
|
133
164
|
const firstItem = event.currentTarget
|
|
@@ -138,15 +169,15 @@ function TabStripSearchDropdown({ className, triggerClassName, autoFocus = true,
|
|
|
138
169
|
else {
|
|
139
170
|
event.stopPropagation();
|
|
140
171
|
}
|
|
141
|
-
}, onKeyUp: (event) => event.stopPropagation(), className: "border-none text-xs shadow-none focus-visible:ring-0", placeholder: "Search...", "aria-label": "Search", autoFocus: autoFocus })] }), _jsx(DropdownMenuSeparator, { className: "flex-shrink-0" }), _jsx("div", { className: "overflow-y-auto", children: isSearching ? (_jsx(DropdownTabItems, { tabs: filteredTabs, emptyMessage:
|
|
172
|
+
}, onKeyUp: (event) => event.stopPropagation(), className: "border-none text-xs shadow-none focus-visible:ring-0", placeholder: "Search...", "aria-label": "Search", autoFocus: autoFocus })] }), _jsx(DropdownMenuSeparator, { className: "flex-shrink-0" }), _jsx("div", { className: "overflow-y-auto overflow-x-hidden", children: isSearching ? (filteredTabs.length === 0 ? (_jsx(DropdownTabItems, { tabs: filteredTabs, emptyMessage: searchEmptyMessage, onTabClick: handleTabClick, renderActions: renderSearchItemActions })) : (_jsxs(_Fragment, { children: [_jsx(DropdownTabItems, { tabs: filteredOpenTabs, onTabClick: handleTabClick, renderActions: renderOpenTabActions }), filteredClosedTabs.length > 0 && (_jsxs(_Fragment, { children: [filteredOpenTabs.length > 0 && _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuLabel, { className: "text-muted-foreground py-1 text-xs font-medium", children: closedTabsLabel }), _jsx(DropdownTabItems, { tabs: filteredClosedTabs, onTabClick: handleTabClick, renderActions: renderSearchItemActions, getItemClassName: () => 'text-muted-foreground' })] }))] }))) : (_jsx(_Fragment, { children: hasAnyTabs ? (_jsxs(_Fragment, { children: [_jsx(DropdownTabItems, { tabs: openTabsList, onTabClick: handleTabClick, renderActions: renderOpenTabActions }), closedTabsList.length > 0 && (_jsxs(_Fragment, { children: [openTabsList.length > 0 && _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuLabel, { className: "text-muted-foreground py-1 text-xs font-medium", children: closedTabsLabel }), _jsx(DropdownTabItems, { tabs: closedTabsList, onTabClick: handleTabClick, renderActions: renderSearchItemActions, getItemClassName: () => 'text-muted-foreground' })] }))] })) : (_jsx(DropdownTabItems, { tabs: [], emptyMessage: emptyMessage, onTabClick: handleTabClick, renderActions: renderSearchItemActions })) })) })] })] }));
|
|
142
173
|
}
|
|
143
|
-
function DropdownTabItems({ tabs, emptyMessage, onTabClick, renderActions, }) {
|
|
174
|
+
function DropdownTabItems({ tabs, emptyMessage, onTabClick, renderActions, getItemClassName, }) {
|
|
144
175
|
if (tabs.length === 0) {
|
|
145
176
|
if (!emptyMessage)
|
|
146
177
|
return null;
|
|
147
178
|
return (_jsx(DropdownMenuLabel, { className: "items-center justify-center text-xs", children: emptyMessage }));
|
|
148
179
|
}
|
|
149
|
-
return (_jsx(_Fragment, { children: tabs.map((tab) => (_jsxs(DropdownMenuItem, { onClick: () => onTabClick?.(tab.id), className:
|
|
180
|
+
return (_jsx(_Fragment, { children: tabs.map((tab) => (_jsxs(DropdownMenuItem, { onClick: () => onTabClick?.(tab.id), className: cn('flex h-7 cursor-pointer items-center justify-between truncate', getItemClassName?.(tab)), children: [_jsx("span", { className: "xs truncate pl-1", children: tab.name }), renderActions && (_jsx("div", { className: "flex items-center gap-2", children: renderActions(tab) }))] }, tab.id))) }));
|
|
150
181
|
}
|
|
151
182
|
/**
|
|
152
183
|
* Renders a button to create a new tab.
|
|
@@ -188,11 +219,13 @@ function TabStripRoot({ className, tabsListClassName, children, tabs, openTabs,
|
|
|
188
219
|
const [editingTabId, setEditingTabId] = useState(null);
|
|
189
220
|
const scrollContainerRef = useRef(null);
|
|
190
221
|
const prevSelectedIdRef = useRef(null);
|
|
222
|
+
const prevOpenTabIdsRef = useRef(new Set());
|
|
223
|
+
const lastOpenedAtRef = useRef(new Map());
|
|
191
224
|
const openTabsSet = useMemo(() => new Set(openTabs), [openTabs]);
|
|
192
225
|
// Build openTabItems in the order of openTabs (for drag-to-reorder)
|
|
193
226
|
const openTabItems = useMemo(() => {
|
|
194
227
|
const tabsById = new Map(tabs.map((tab) => [tab.id, tab]));
|
|
195
|
-
return openTabs
|
|
228
|
+
return (openTabs ?? [])
|
|
196
229
|
.map((id) => tabsById.get(id))
|
|
197
230
|
.filter((tab) => tab !== undefined);
|
|
198
231
|
}, [tabs, openTabs]);
|
|
@@ -202,8 +235,23 @@ function TabStripRoot({ className, tabsListClassName, children, tabs, openTabs,
|
|
|
202
235
|
const filteredTabs = useMemo(() => trimmedSearch
|
|
203
236
|
? tabs.filter((tab) => tab.name.toLowerCase().includes(trimmedSearch))
|
|
204
237
|
: [], [tabs, trimmedSearch]);
|
|
205
|
-
// Auto-scroll to selected tab
|
|
206
238
|
useEffect(() => {
|
|
239
|
+
if (!selectedTabId)
|
|
240
|
+
return;
|
|
241
|
+
lastOpenedAtRef.current.set(selectedTabId, Date.now());
|
|
242
|
+
}, [selectedTabId]);
|
|
243
|
+
useEffect(() => {
|
|
244
|
+
const ids = new Set(tabs.map((tab) => tab.id));
|
|
245
|
+
const next = new Map();
|
|
246
|
+
for (const [id, ts] of lastOpenedAtRef.current.entries()) {
|
|
247
|
+
if (ids.has(id)) {
|
|
248
|
+
next.set(id, ts);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
lastOpenedAtRef.current = next;
|
|
252
|
+
}, [tabs]);
|
|
253
|
+
// Auto-scroll to selected tab
|
|
254
|
+
useLayoutEffect(() => {
|
|
207
255
|
if (!selectedTabId)
|
|
208
256
|
return;
|
|
209
257
|
if (prevSelectedIdRef.current === selectedTabId)
|
|
@@ -215,7 +263,8 @@ function TabStripRoot({ className, tabsListClassName, children, tabs, openTabs,
|
|
|
215
263
|
const isOpen = openTabItems.some((tab) => tab.id === selectedTabId);
|
|
216
264
|
if (!isOpen)
|
|
217
265
|
return;
|
|
218
|
-
|
|
266
|
+
// Use queueMicrotask to defer scroll until after Radix UI updates the DOM
|
|
267
|
+
queueMicrotask(() => {
|
|
219
268
|
const activeTab = container.querySelector('[data-state="active"]');
|
|
220
269
|
if (!activeTab)
|
|
221
270
|
return;
|
|
@@ -225,10 +274,41 @@ function TabStripRoot({ className, tabsListClassName, children, tabs, openTabs,
|
|
|
225
274
|
inline: 'nearest',
|
|
226
275
|
});
|
|
227
276
|
});
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
277
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
278
|
+
}, [selectedTabId]);
|
|
279
|
+
// Auto-scroll to newly added tab
|
|
280
|
+
useLayoutEffect(() => {
|
|
281
|
+
const container = scrollContainerRef.current;
|
|
282
|
+
if (!container)
|
|
283
|
+
return;
|
|
284
|
+
// Find newly added tabs (in openTabs but not in prevOpenTabIdsRef)
|
|
285
|
+
const newTabIds = (openTabs ?? []).filter((id) => !prevOpenTabIdsRef.current.has(id));
|
|
286
|
+
// Update ref for next comparison
|
|
287
|
+
prevOpenTabIdsRef.current = new Set(openTabs);
|
|
288
|
+
if (newTabIds.length > 0) {
|
|
289
|
+
const now = Date.now();
|
|
290
|
+
for (const id of newTabIds) {
|
|
291
|
+
lastOpenedAtRef.current.set(id, now);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
// Skip scroll on initial render (when ref was empty, all tabs appear "new")
|
|
295
|
+
if (newTabIds.length === (openTabs?.length ?? 0))
|
|
296
|
+
return;
|
|
297
|
+
// If there are new tabs, scroll to the last one added
|
|
298
|
+
if (newTabIds.length === 0)
|
|
299
|
+
return;
|
|
300
|
+
const newTabId = newTabIds[newTabIds.length - 1];
|
|
301
|
+
queueMicrotask(() => {
|
|
302
|
+
const newTabElement = container.querySelector(`[data-tab-id="${newTabId}"]`);
|
|
303
|
+
if (!newTabElement)
|
|
304
|
+
return;
|
|
305
|
+
newTabElement.scrollIntoView({
|
|
306
|
+
behavior: 'smooth',
|
|
307
|
+
block: 'nearest',
|
|
308
|
+
inline: 'nearest',
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
}, [openTabs]);
|
|
232
312
|
const handleInlineRename = (tabId, newName) => {
|
|
233
313
|
if (!onRename)
|
|
234
314
|
return;
|
|
@@ -273,6 +353,7 @@ function TabStripRoot({ className, tabsListClassName, children, tabs, openTabs,
|
|
|
273
353
|
selectedTabId,
|
|
274
354
|
openTabs,
|
|
275
355
|
preventCloseLastTab,
|
|
356
|
+
getLastOpenedAt: (tabId) => lastOpenedAtRef.current.get(tabId),
|
|
276
357
|
onOpenTabsChange,
|
|
277
358
|
onSelect,
|
|
278
359
|
onCreate,
|
|
@@ -289,7 +370,7 @@ function TabStripRoot({ className, tabsListClassName, children, tabs, openTabs,
|
|
|
289
370
|
const handleValueChange = (value) => {
|
|
290
371
|
onSelect?.(value);
|
|
291
372
|
};
|
|
292
|
-
return (_jsx(Tabs, { value: selectedTabId ?? undefined, onValueChange: handleValueChange, className: cn('bg-muted w-full min-w-0', className), children: _jsx(TabStripContext.Provider, { value: contextValue, children: _jsx(TabsList, { className: cn('flex w-full min-w-0 justify-start gap-2 bg-transparent p-0', tabsListClassName), children: children ?? (_jsxs(_Fragment, { children: [_jsx(TabStripSearchDropdown, {}), _jsx(TabStripTabs, {}), _jsx(TabStripNewButton, {})] })) }) }) }));
|
|
373
|
+
return (_jsx(Tabs, { value: selectedTabId ?? undefined, onValueChange: handleValueChange, activationMode: "manual", className: cn('bg-muted w-full min-w-0', className), children: _jsx(TabStripContext.Provider, { value: contextValue, children: _jsx(TabsList, { className: cn('flex h-9 w-full min-w-0 items-center justify-start gap-2 overflow-visible bg-transparent p-0', tabsListClassName), children: children ?? (_jsxs(_Fragment, { children: [_jsx(TabStripSearchDropdown, {}), _jsx(TabStripTabs, {}), _jsx(TabStripNewButton, {})] })) }) }) }));
|
|
293
374
|
}
|
|
294
375
|
// Attach subcomponents
|
|
295
376
|
export const TabStrip = Object.assign(TabStripRoot, {
|