@knymbus/voxel-ui 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.storybook/main.ts +17 -0
- package/.storybook/preview.ts +23 -0
- package/debug-storybook.log +40 -0
- package/dist/chunks/jsx-runtime-Boo2vksn.js +182 -0
- package/dist/chunks/resizable-ImB8dfG_.js +112 -0
- package/dist/chunks/tabs-MaVN00hJ.js +86 -0
- package/dist/components/button/Button.d.ts +31 -0
- package/dist/components/button/ButtonGroup.d.ts +12 -0
- package/dist/components/button/SplitActionButton.d.ts +2 -0
- package/dist/components/button/index.d.ts +5 -0
- package/dist/components/button/split-types.d.ts +16 -0
- package/dist/components/button/types.d.ts +9 -0
- package/dist/components/icons/AddIcon.d.ts +2 -0
- package/dist/components/icons/BlankDocIcon.d.ts +2 -0
- package/dist/components/icons/ChatIcon.d.ts +2 -0
- package/dist/components/icons/ChevronDownIcon.d.ts +2 -0
- package/dist/components/icons/CloseIcon.d.ts +2 -0
- package/dist/components/icons/CommentIcon.d.ts +2 -0
- package/dist/components/icons/DeleteChatIcon.d.ts +2 -0
- package/dist/components/icons/DocumentIcon.d.ts +2 -0
- package/dist/components/icons/ExpandIcon.d.ts +2 -0
- package/dist/components/icons/FolderIcon.d.ts +2 -0
- package/dist/components/icons/GroupIcon.d.ts +2 -0
- package/dist/components/icons/MinimizeIcon.d.ts +2 -0
- package/dist/components/icons/MinusIcon.d.ts +2 -0
- package/dist/components/icons/MoreIcon.d.ts +2 -0
- package/dist/components/icons/OpenFolderIcon.d.ts +2 -0
- package/dist/components/icons/PersonIcon.d.ts +2 -0
- package/dist/components/icons/PlusChatIcon.d.ts +2 -0
- package/dist/components/icons/PlusCommentIcon.d.ts +2 -0
- package/dist/components/icons/PlusDocBadgeIcon.d.ts +8 -0
- package/dist/components/icons/PlusDocIcon.d.ts +2 -0
- package/dist/components/icons/PlusPersonIcon.d.ts +2 -0
- package/dist/components/icons/RefreshIcon.d.ts +2 -0
- package/dist/components/icons/SearchIcon.d.ts +2 -0
- package/dist/components/icons/TerminalIcon.d.ts +2 -0
- package/dist/components/icons/TrashIcon.d.ts +2 -0
- package/dist/components/icons/TruckIcon.d.ts +2 -0
- package/dist/components/icons/index.d.ts +26 -0
- package/dist/components/icons/types.d.ts +5 -0
- package/dist/components/resizable/ResizablePanel.d.ts +10 -0
- package/dist/components/resizable/index.d.ts +1 -0
- package/dist/components/resizable/index.js +2 -0
- package/dist/components/search/SearchInput.d.ts +10 -0
- package/dist/components/search/index.d.ts +2 -0
- package/dist/components/search/types.d.ts +19 -0
- package/dist/components/tabs/TabButton.d.ts +12 -0
- package/dist/components/tabs/TabButtonGroup.d.ts +9 -0
- package/dist/components/tabs/TabPanel.d.ts +8 -0
- package/dist/components/tabs/TabPanelList.d.ts +11 -0
- package/dist/components/tabs/index.d.ts +5 -0
- package/dist/components/tabs/index.js +2 -0
- package/dist/components/tabs/types.d.ts +9 -0
- package/dist/components/tabs/useTab.d.ts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +1071 -0
- package/package.json +68 -0
- package/src/components/button/Button.stories.tsx +70 -0
- package/src/components/button/Button.tsx +108 -0
- package/src/components/button/ButtonGroup.stories.tsx +63 -0
- package/src/components/button/ButtonGroup.tsx +62 -0
- package/src/components/button/SplitActionButton.tsx +116 -0
- package/src/components/button/SplitButton.stories.tsx +55 -0
- package/src/components/button/index.ts +7 -0
- package/src/components/button/split-types.ts +18 -0
- package/src/components/button/types.ts +10 -0
- package/src/components/icons/AddIcon.tsx +10 -0
- package/src/components/icons/BlankDocIcon.tsx +10 -0
- package/src/components/icons/ChatIcon.tsx +9 -0
- package/src/components/icons/ChevronDownIcon.tsx +9 -0
- package/src/components/icons/CloseIcon.tsx +23 -0
- package/src/components/icons/CommentIcon.tsx +9 -0
- package/src/components/icons/DeleteChatIcon.tsx +10 -0
- package/src/components/icons/DocumentIcon.tsx +13 -0
- package/src/components/icons/ExpandIcon.tsx +12 -0
- package/src/components/icons/FolderIcon.tsx +9 -0
- package/src/components/icons/GroupIcon.tsx +12 -0
- package/src/components/icons/Icon.stories.tsx +122 -0
- package/src/components/icons/MinimizeIcon.tsx +12 -0
- package/src/components/icons/MinusIcon.tsx +9 -0
- package/src/components/icons/MoreIcon.tsx +11 -0
- package/src/components/icons/OpenFolderIcon.tsx +37 -0
- package/src/components/icons/PersonIcon.tsx +10 -0
- package/src/components/icons/PlusChatIcon.tsx +11 -0
- package/src/components/icons/PlusCommentIcon.tsx +11 -0
- package/src/components/icons/PlusDocBadgeIcon.tsx +74 -0
- package/src/components/icons/PlusDocIcon.tsx +12 -0
- package/src/components/icons/PlusPersonIcon.tsx +12 -0
- package/src/components/icons/RefreshIcon.tsx +9 -0
- package/src/components/icons/SearchIcon.tsx +10 -0
- package/src/components/icons/TerminalIcon.tsx +11 -0
- package/src/components/icons/TrashIcon.tsx +12 -0
- package/src/components/icons/TruckIcon.tsx +12 -0
- package/src/components/icons/index.ts +26 -0
- package/src/components/icons/types.ts +6 -0
- package/src/components/resizable/ResizablePanel.tsx +183 -0
- package/src/components/resizable/index.ts +1 -0
- package/src/components/search/SearchInput.stories.tsx +91 -0
- package/src/components/search/SearchInput.tsx +254 -0
- package/src/components/search/index.ts +2 -0
- package/src/components/search/types.ts +21 -0
- package/src/components/tabs/TabButton.tsx +56 -0
- package/src/components/tabs/TabButtonGroup.tsx +82 -0
- package/src/components/tabs/TabPanel.tsx +44 -0
- package/src/components/tabs/TabPanelList.tsx +31 -0
- package/src/components/tabs/Tabs.stories.tsx +71 -0
- package/src/components/tabs/index.ts +5 -0
- package/src/components/tabs/types.ts +10 -0
- package/src/components/tabs/useTab.ts +33 -0
- package/src/index.css +35 -0
- package/src/index.ts +5 -0
- package/src/vite-env.d.ts +5 -0
- package/tsconfig.json +47 -0
- package/vite.config.ts +64 -0
- package/vitest.shims.d.ts +1 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
type PullPlacement = 'right' | 'left' | 'top' | 'bottom';
|
|
4
|
+
|
|
5
|
+
interface ResizablePanelProps {
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
placement?: PullPlacement;
|
|
8
|
+
defaultSize?: number;
|
|
9
|
+
minSize?: number;
|
|
10
|
+
maxSize?: number; // Optional limit: if omitted, goes 100% of parent container width/height
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default function ResizablePanel({
|
|
14
|
+
children,
|
|
15
|
+
placement = 'right',
|
|
16
|
+
defaultSize = 240,
|
|
17
|
+
minSize = 150,
|
|
18
|
+
maxSize
|
|
19
|
+
}: ResizablePanelProps) {
|
|
20
|
+
const panelRef = useRef<HTMLDivElement | null>(null);
|
|
21
|
+
const [panelSize, setPanelSize] = useState<number>(defaultSize);
|
|
22
|
+
const [isResizing, setIsResizing] = useState<boolean>(false);
|
|
23
|
+
const [isHovered, setIsHovered] = useState<boolean>(false);
|
|
24
|
+
const [isCollapsed, setIsCollapsed] = useState<boolean>(false);
|
|
25
|
+
|
|
26
|
+
const isHorizontal = placement === 'right' || placement === 'left';
|
|
27
|
+
const cursorStyle = isHorizontal ? 'ew-resize' : 'ns-resize';
|
|
28
|
+
|
|
29
|
+
const CLOSE_SNAP_THRESHOLD = 60;
|
|
30
|
+
const MAX_SNAP_THRESHOLD = 60;
|
|
31
|
+
|
|
32
|
+
const startResizing = useCallback((e: React.MouseEvent) => {
|
|
33
|
+
e.preventDefault();
|
|
34
|
+
e.stopPropagation();
|
|
35
|
+
setIsResizing(true);
|
|
36
|
+
}, []);
|
|
37
|
+
|
|
38
|
+
const stopResizing = useCallback(() => {
|
|
39
|
+
setIsResizing(false);
|
|
40
|
+
}, []);
|
|
41
|
+
|
|
42
|
+
const handleDoubleClickReset = useCallback((e: React.MouseEvent) => {
|
|
43
|
+
e.preventDefault();
|
|
44
|
+
setIsCollapsed(false);
|
|
45
|
+
setPanelSize(defaultSize);
|
|
46
|
+
}, [defaultSize]);
|
|
47
|
+
|
|
48
|
+
const resize = useCallback((e: MouseEvent) => {
|
|
49
|
+
if (!isResizing || !panelRef.current || !panelRef.current.parentElement) return;
|
|
50
|
+
|
|
51
|
+
const parentBounds = panelRef.current.parentElement.getBoundingClientRect();
|
|
52
|
+
const parentWidth = parentBounds.width;
|
|
53
|
+
const parentHeight = parentBounds.height;
|
|
54
|
+
|
|
55
|
+
let calculatedSize = defaultSize;
|
|
56
|
+
let absoluteParentLimit = isHorizontal ? parentWidth : parentHeight;
|
|
57
|
+
|
|
58
|
+
// Fallback Routing: Use the user's custom maxSize if defined, otherwise go full parent boundary length
|
|
59
|
+
let dynamicMaxLimit = maxSize !== undefined ? Math.min(maxSize, absoluteParentLimit) : absoluteParentLimit;
|
|
60
|
+
|
|
61
|
+
switch (placement) {
|
|
62
|
+
case 'right':
|
|
63
|
+
calculatedSize = e.clientX - parentBounds.left;
|
|
64
|
+
break;
|
|
65
|
+
case 'left':
|
|
66
|
+
calculatedSize = parentBounds.right - e.clientX;
|
|
67
|
+
break;
|
|
68
|
+
case 'bottom':
|
|
69
|
+
calculatedSize = e.clientY - parentBounds.top;
|
|
70
|
+
break;
|
|
71
|
+
case 'top':
|
|
72
|
+
calculatedSize = parentBounds.bottom - e.clientY;
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 1. Lower Close Snap-Zone Audit
|
|
77
|
+
if (calculatedSize < CLOSE_SNAP_THRESHOLD) {
|
|
78
|
+
setPanelSize(0);
|
|
79
|
+
setIsCollapsed(true);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 2. Upper Max Snap-Zone Audit
|
|
84
|
+
if (calculatedSize > dynamicMaxLimit - MAX_SNAP_THRESHOLD) {
|
|
85
|
+
setPanelSize(dynamicMaxLimit);
|
|
86
|
+
setIsCollapsed(false);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 3. Guardrails Path Routing
|
|
91
|
+
setIsCollapsed(false);
|
|
92
|
+
if (calculatedSize < minSize) {
|
|
93
|
+
setPanelSize(minSize);
|
|
94
|
+
} else if (calculatedSize > dynamicMaxLimit) {
|
|
95
|
+
setPanelSize(dynamicMaxLimit);
|
|
96
|
+
} else {
|
|
97
|
+
setPanelSize(calculatedSize);
|
|
98
|
+
}
|
|
99
|
+
}, [isResizing, placement, minSize, maxSize, defaultSize, isHorizontal]);
|
|
100
|
+
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
if (isResizing) {
|
|
103
|
+
window.addEventListener('mousemove', resize);
|
|
104
|
+
window.addEventListener('mouseup', stopResizing);
|
|
105
|
+
document.body.style.cursor = cursorStyle;
|
|
106
|
+
document.body.style.userSelect = 'none';
|
|
107
|
+
} else {
|
|
108
|
+
document.body.style.cursor = '';
|
|
109
|
+
document.body.style.userSelect = '';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return () => {
|
|
113
|
+
window.removeEventListener('mousemove', resize);
|
|
114
|
+
window.removeEventListener('mouseup', stopResizing);
|
|
115
|
+
document.body.style.cursor = '';
|
|
116
|
+
document.body.style.userSelect = '';
|
|
117
|
+
};
|
|
118
|
+
}, [isResizing, resize, stopResizing, cursorStyle]);
|
|
119
|
+
|
|
120
|
+
const currentRenderSize = isCollapsed ? 0 : panelSize;
|
|
121
|
+
|
|
122
|
+
// FIXED SYNC: Stripped the Tailwind transition utility class line to make sizing responsive instantly
|
|
123
|
+
const sizeStyle = isHorizontal
|
|
124
|
+
? { width: `${currentRenderSize}px` }
|
|
125
|
+
: { height: `${currentRenderSize}px` };
|
|
126
|
+
|
|
127
|
+
const getHandleInlineStyles = (): React.CSSProperties => {
|
|
128
|
+
const trackBoxPaddingSize = 12;
|
|
129
|
+
const centerOffset = -(trackBoxPaddingSize / 2);
|
|
130
|
+
|
|
131
|
+
const baseStyles: React.CSSProperties = {
|
|
132
|
+
position: 'absolute',
|
|
133
|
+
zIndex: 50,
|
|
134
|
+
cursor: cursorStyle,
|
|
135
|
+
pointerEvents: 'all',
|
|
136
|
+
backgroundColor: 'transparent',
|
|
137
|
+
display: 'flex',
|
|
138
|
+
alignItems: 'center',
|
|
139
|
+
justifyContent: 'center',
|
|
140
|
+
userSelect: 'none'
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
if (placement === 'right') {
|
|
144
|
+
return { ...baseStyles, top: 0, bottom: 0, width: `${trackBoxPaddingSize}px`, left: `${currentRenderSize + centerOffset}px` };
|
|
145
|
+
}
|
|
146
|
+
if (placement === 'left') {
|
|
147
|
+
return { ...baseStyles, top: 0, bottom: 0, width: `${trackBoxPaddingSize}px`, left: `${centerOffset}px` };
|
|
148
|
+
}
|
|
149
|
+
if (placement === 'bottom') {
|
|
150
|
+
return { ...baseStyles, left: 0, right: 0, height: `${trackBoxPaddingSize}px`, top: `${currentRenderSize + centerOffset}px`, flexDirection: 'column' };
|
|
151
|
+
}
|
|
152
|
+
return { ...baseStyles, left: 0, right: 0, height: `${trackBoxPaddingSize}px`, top: `${centerOffset}px`, flexDirection: 'column' };
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const lineClasses = isHorizontal
|
|
156
|
+
? `w-[2px] h-full ${isResizing || isHovered ? 'bg-vsc-accent' : 'bg-vsc-border'}`
|
|
157
|
+
: `h-[2px] w-full ${isResizing || isHovered ? 'bg-vsc-accent' : 'bg-vsc-border'}`;
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<div
|
|
161
|
+
ref={panelRef}
|
|
162
|
+
className="relative shrink-0"
|
|
163
|
+
style={sizeStyle}
|
|
164
|
+
>
|
|
165
|
+
{!isCollapsed && (
|
|
166
|
+
<div className="w-full h-full overflow-hidden">
|
|
167
|
+
{children}
|
|
168
|
+
</div>
|
|
169
|
+
)}
|
|
170
|
+
|
|
171
|
+
{/* Draggable Handle Track Rail */}
|
|
172
|
+
<div
|
|
173
|
+
onMouseDown={startResizing}
|
|
174
|
+
onDoubleClick={handleDoubleClickReset}
|
|
175
|
+
onMouseEnter={() => setIsHovered(true)}
|
|
176
|
+
onMouseLeave={() => setIsHovered(false)}
|
|
177
|
+
style={getHandleInlineStyles()}
|
|
178
|
+
>
|
|
179
|
+
<div className={lineClasses} />
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as ResizablePanel } from './ResizablePanel';
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
3
|
+
import SearchInput from './SearchInput';
|
|
4
|
+
import { SearchResultItem } from './types';
|
|
5
|
+
import Button from '../button/Button';
|
|
6
|
+
|
|
7
|
+
const meta: Meta<typeof SearchInput> = {
|
|
8
|
+
title: 'Inputs/VoxelSearchInputSuite',
|
|
9
|
+
component: SearchInput,
|
|
10
|
+
parameters: {
|
|
11
|
+
layout: 'centered',
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
export default meta;
|
|
15
|
+
|
|
16
|
+
const MOCK_DATASET: SearchResultItem[] = [
|
|
17
|
+
{ id: '1', title: '#TRK-771239', subtitle: 'Consignee: Sarah Jenkins Logistics • Gross Wt: 24.5kg', category: 'In Transit' },
|
|
18
|
+
{ id: '2', title: '#TRK-771242', subtitle: 'Consignee: Elena Rostova Import LLC • Gross Wt: 8.2kg', category: 'Delivered' },
|
|
19
|
+
{ id: '3', title: '#TRK-661041', subtitle: 'Consignee: David Chang Distributions • Gross Wt: 114kg', category: 'Delayed' }
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const MultiSearchWorkspace = () => {
|
|
23
|
+
const [simpleQuery, setSimpleQuery] = useState<string>('');
|
|
24
|
+
const [floatQuery, setSimpleFloatQuery] = useState<string>('');
|
|
25
|
+
const [menuQuery, setSimpleMenuQuery] = useState<string>('');
|
|
26
|
+
|
|
27
|
+
// Local matching array filtering down lines for the menu dropdown display logic loop
|
|
28
|
+
const filteredMenuResults = MOCK_DATASET.filter(item =>
|
|
29
|
+
item.title.toLowerCase().includes(menuQuery.toLowerCase()) ||
|
|
30
|
+
item.subtitle?.toLowerCase().includes(menuQuery.toLowerCase())
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
const ResultPanel = () => {
|
|
35
|
+
return (
|
|
36
|
+
<div className="flex flex-row items-center justify-between">
|
|
37
|
+
<div>Showing {filteredMenuResults.length} Manifest records</div>
|
|
38
|
+
<Button size='xs' color='primary' onClick={() => alert("Show more result clicked")}>View More</Button>
|
|
39
|
+
</div>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div className="p-8 bg-vsc-sidebar border border-vsc-border rounded-md space-y-8 min-w-112.5 text-vsc-text">
|
|
45
|
+
|
|
46
|
+
{/* 1. Simple Variant Bracket */}
|
|
47
|
+
<div className="space-y-1.5">
|
|
48
|
+
<p className="text-[10px] font-bold text-vsc-muted uppercase tracking-wider">1. Simple Matrix Variant with Count Log</p>
|
|
49
|
+
<SearchInput
|
|
50
|
+
variant="simple"
|
|
51
|
+
value={simpleQuery}
|
|
52
|
+
onChange={setSimpleQuery}
|
|
53
|
+
resultsCount={simpleQuery ? 14 : 0}
|
|
54
|
+
placeholder="Type track ID to evaluate records count..."
|
|
55
|
+
resultIndicatorPanel={<ResultPanel />}
|
|
56
|
+
/>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
{/* 2. Float Expandable Bracket */}
|
|
60
|
+
<div className="space-y-1.5 flex flex-col items-start">
|
|
61
|
+
<p className="text-[10px] font-bold text-vsc-muted uppercase tracking-wider">2. Float Icon Variant (Expands on Click Header Line)</p>
|
|
62
|
+
<SearchInput
|
|
63
|
+
variant="float"
|
|
64
|
+
value={floatQuery}
|
|
65
|
+
onChange={setSimpleFloatQuery}
|
|
66
|
+
placeholder="Expandable terminal search..."
|
|
67
|
+
showFloatPeek
|
|
68
|
+
resultIndicatorPanel={<ResultPanel />}
|
|
69
|
+
/>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
{/* 3. Dropdown Menu Variant Bracket */}
|
|
73
|
+
<div className="space-y-1.5">
|
|
74
|
+
<p className="text-[10px] font-bold text-vsc-muted uppercase tracking-wider">3. Dropdown Menu Variant (Type '#TRK' to open overlay rows)</p>
|
|
75
|
+
<SearchInput
|
|
76
|
+
variant="menu"
|
|
77
|
+
value={menuQuery}
|
|
78
|
+
onChange={setSimpleMenuQuery}
|
|
79
|
+
menuResults={filteredMenuResults}
|
|
80
|
+
onResultClick={(item) => alert(`Selected manifest tracking entity node: ${item.title}`)}
|
|
81
|
+
onViewMoreClick={() => alert('View more clicked! Redirecting system scope path to Item Statuses grid tab board.')}
|
|
82
|
+
/>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export const CompleteSearchSuiteDemo: StoryObj = {
|
|
90
|
+
render: () => <MultiSearchWorkspace />,
|
|
91
|
+
};
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import React, { useState, useRef, useEffect, ReactNode } from 'react';
|
|
2
|
+
import { SearchInputProps } from './types';
|
|
3
|
+
import { Search, Close } from '../icons';
|
|
4
|
+
import { Button } from '../button/Button';
|
|
5
|
+
|
|
6
|
+
// Extend your props declaration interface locally to support the new feature flags
|
|
7
|
+
interface AdvancedSearchInputProps extends Omit<SearchInputProps, 'variant'> {
|
|
8
|
+
variant?: 'simple' | 'float' | 'menu';
|
|
9
|
+
showFloatPeek?: boolean; // When true, float variant slides down a results indicator bar
|
|
10
|
+
resultIndicatorPanel?: string | ReactNode
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default function SearchInput({
|
|
14
|
+
variant = 'simple',
|
|
15
|
+
value,
|
|
16
|
+
onChange,
|
|
17
|
+
onClear,
|
|
18
|
+
placeholder = 'Search manifests or tracking codes...',
|
|
19
|
+
resultsCount = 0,
|
|
20
|
+
menuResults = [],
|
|
21
|
+
onResultClick,
|
|
22
|
+
onViewMoreClick,
|
|
23
|
+
className = '',
|
|
24
|
+
showFloatPeek = false, // Optional feature flag default boundary
|
|
25
|
+
resultIndicatorPanel
|
|
26
|
+
}: AdvancedSearchInputProps) {
|
|
27
|
+
const [isExpanded, setIsExpanded] = useState<boolean>(false);
|
|
28
|
+
const [isMenuOpen, setIsMenuOpen] = useState<boolean>(false);
|
|
29
|
+
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
30
|
+
const inputRef = useRef<HTMLInputElement | null>(null);
|
|
31
|
+
|
|
32
|
+
// Auto-close overlay dropdowns if the user clicks completely out of the component framework bounding box
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
const handleOutsideClick = (e: MouseEvent) => {
|
|
35
|
+
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
|
|
36
|
+
setIsMenuOpen(false);
|
|
37
|
+
if (variant === 'float' && value === '') {
|
|
38
|
+
setIsExpanded(false);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
window.addEventListener('mousedown', handleOutsideClick);
|
|
43
|
+
return () => window.removeEventListener('mousedown', handleOutsideClick);
|
|
44
|
+
}, [variant, value]);
|
|
45
|
+
|
|
46
|
+
const handleClearTrigger = (e: React.MouseEvent) => {
|
|
47
|
+
e.stopPropagation();
|
|
48
|
+
onChange('');
|
|
49
|
+
if (onClear) onClear();
|
|
50
|
+
if (inputRef.current) inputRef.current.focus();
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const handleFloatActivation = () => {
|
|
54
|
+
setIsExpanded(true);
|
|
55
|
+
setTimeout(() => inputRef.current?.focus(), 50);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const baseWrapper = "relative flex flex-col font-sans text-xs select-none";
|
|
59
|
+
|
|
60
|
+
// --- 1. SIMPLE VARIANT PANEL ---
|
|
61
|
+
if (variant === 'simple') {
|
|
62
|
+
const hasResults = resultsCount > 0 && value.length > 0;
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<div className={`relative ${baseWrapper} ${className}`.trim()}>
|
|
66
|
+
{/* Core Input Box Frame */}
|
|
67
|
+
<div className="relative flex items-center bg-vsc-bg-input border border-vsc-border rounded-sm h-8 group hover:border-vsc-accent transition-colors z-20">
|
|
68
|
+
<Search size={14} className="absolute left-2.5 text-vsc-muted group-hover:text-vsc-text" />
|
|
69
|
+
<input
|
|
70
|
+
ref={inputRef}
|
|
71
|
+
type="text"
|
|
72
|
+
value={value}
|
|
73
|
+
onChange={(e) => onChange(e.target.value)}
|
|
74
|
+
placeholder={placeholder}
|
|
75
|
+
className="w-full h-full pl-8 pr-10 bg-transparent text-vsc-text border-none outline-none focus:outline-none placeholder-vsc-muted"
|
|
76
|
+
/>
|
|
77
|
+
{value && (
|
|
78
|
+
<div className="absolute right-1 top-1/2 -translate-y-1/2 flex items-center">
|
|
79
|
+
<Button
|
|
80
|
+
icon={Close}
|
|
81
|
+
color="ghost"
|
|
82
|
+
iconOnly={true}
|
|
83
|
+
onClick={handleClearTrigger}
|
|
84
|
+
size="xs"
|
|
85
|
+
/>
|
|
86
|
+
</div>
|
|
87
|
+
)}
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
{/*
|
|
91
|
+
🎛️ HIGH-FIDELITY FLOATING SLIDE-DOWN INDICATOR PANEL
|
|
92
|
+
- Uses 'absolute' so it zero-impacts the parent height layout block.
|
|
93
|
+
- Utilizes hardware-accelerated transforms (translateY) for a smooth glide effect.
|
|
94
|
+
*/}
|
|
95
|
+
<div
|
|
96
|
+
className={`absolute left-0 right-0 top-8 z-10 overflow-hidden transition-all duration-200 cubic-bezier(0.34, 1.56, 0.64, 1) ${!hasResults ? 'pointer-events-none' : ''}`}
|
|
97
|
+
style={{
|
|
98
|
+
height: hasResults ? 'auto' : '0px',
|
|
99
|
+
opacity: hasResults ? 1 : 0,
|
|
100
|
+
transform: hasResults ? 'translateY(0px)' : 'translateY(-4px)'
|
|
101
|
+
}}
|
|
102
|
+
>
|
|
103
|
+
<div className="bg-vsc-sidebar shadow-sm p-1.5 pt-1 ">
|
|
104
|
+
{
|
|
105
|
+
resultIndicatorPanel ? resultIndicatorPanel : (
|
|
106
|
+
<p className="text-[10px] font-mono text-vsc-muted pl-1 truncate">
|
|
107
|
+
Showing <span className="text-vsc-accent font-bold">{resultsCount}</span> matching database metrics records
|
|
108
|
+
</p>
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// --- 2. FLOAT EXPANDABLE VARIANT ---
|
|
118
|
+
if (variant === 'float') {
|
|
119
|
+
const showPeekIndicator = showFloatPeek && resultsCount > 0 && value.length > 0;
|
|
120
|
+
return (
|
|
121
|
+
<div ref={containerRef} className={`${baseWrapper} ${className}`.trim()}>
|
|
122
|
+
<div
|
|
123
|
+
className={`flex items-center bg-vsc-bg-input border rounded-sm h-8 transition-all duration-300 ease-in-out overflow-hidden ${isExpanded || value ? 'w-64 border-vsc-accent px-2' : 'w-8 border-transparent bg-transparent justify-center'
|
|
124
|
+
}`}
|
|
125
|
+
>
|
|
126
|
+
{isExpanded || value ? (
|
|
127
|
+
<div className="relative flex items-center w-full h-full">
|
|
128
|
+
<Search size={14} className="text-vsc-text shrink-0" />
|
|
129
|
+
<input
|
|
130
|
+
ref={inputRef}
|
|
131
|
+
type="text"
|
|
132
|
+
value={value}
|
|
133
|
+
onChange={(e) => onChange(e.target.value)}
|
|
134
|
+
placeholder={placeholder}
|
|
135
|
+
className="w-full h-full pl-2 pr-6 bg-transparent text-vsc-text border-none outline-none focus:outline-none placeholder-vsc-muted"
|
|
136
|
+
/>
|
|
137
|
+
{value && (
|
|
138
|
+
<div className="absolute right-0 top-1/2 -translate-y-1/2 flex items-center">
|
|
139
|
+
<Button
|
|
140
|
+
iconOnly={true}
|
|
141
|
+
color="ghost"
|
|
142
|
+
icon={Close}
|
|
143
|
+
onClick={handleClearTrigger}
|
|
144
|
+
size="xs"
|
|
145
|
+
/>
|
|
146
|
+
</div>
|
|
147
|
+
)}
|
|
148
|
+
</div>
|
|
149
|
+
) : (
|
|
150
|
+
<Button
|
|
151
|
+
iconOnly={true}
|
|
152
|
+
size="sm"
|
|
153
|
+
icon={Search}
|
|
154
|
+
onClick={handleFloatActivation}
|
|
155
|
+
color="ghost"
|
|
156
|
+
className="text-vsc-muted hover:text-vsc-text transition-colors"
|
|
157
|
+
title="Open Expandable Search"
|
|
158
|
+
/>
|
|
159
|
+
)}
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
{/* Optional dynamic slider line badge dropdown block */}
|
|
163
|
+
<div
|
|
164
|
+
className="overflow-hidden transition-all duration-200 ease-out w-64 absolute top-8"
|
|
165
|
+
style={{
|
|
166
|
+
height: showPeekIndicator ? '20px' : '0px',
|
|
167
|
+
opacity: showPeekIndicator ? 1 : 0
|
|
168
|
+
}}
|
|
169
|
+
>
|
|
170
|
+
<p className="text-[9px] font-mono text-vsc-accent pt-1 pl-1 truncate bg-vsc-sidebar border border-t-0 border-vsc-border p-1 rounded-b shadow-sm">
|
|
171
|
+
Quick Peek: Found {resultsCount} rows
|
|
172
|
+
</p>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// --- 3. MENU STYLE SEARCH DROPDOWN ---
|
|
179
|
+
return (
|
|
180
|
+
<div ref={containerRef} className={`${baseWrapper} ${className}`.trim()}>
|
|
181
|
+
<div className="relative flex items-center bg-vsc-bg-input border border-vsc-border rounded-sm h-8 focus-within:border-vsc-accent">
|
|
182
|
+
<Search size={14} className="absolute left-2.5 text-vsc-muted" />
|
|
183
|
+
<input
|
|
184
|
+
ref={inputRef}
|
|
185
|
+
type="text"
|
|
186
|
+
value={value}
|
|
187
|
+
onChange={(e) => {
|
|
188
|
+
onChange(e.target.value);
|
|
189
|
+
setIsMenuOpen(true);
|
|
190
|
+
}}
|
|
191
|
+
onFocus={() => setIsMenuOpen(true)}
|
|
192
|
+
placeholder={placeholder}
|
|
193
|
+
className="w-full h-full pl-8 pr-10 bg-transparent text-vsc-text border-none outline-none focus:outline-none placeholder-vsc-muted"
|
|
194
|
+
/>
|
|
195
|
+
{value && (
|
|
196
|
+
<div className="absolute right-1 top-1/2 -translate-y-1/2 flex items-center">
|
|
197
|
+
<Button
|
|
198
|
+
iconOnly={true}
|
|
199
|
+
icon={Close}
|
|
200
|
+
color="ghost"
|
|
201
|
+
onClick={handleClearTrigger}
|
|
202
|
+
size="xs"
|
|
203
|
+
/>
|
|
204
|
+
</div>
|
|
205
|
+
)}
|
|
206
|
+
</div>
|
|
207
|
+
|
|
208
|
+
{isMenuOpen && value && (
|
|
209
|
+
<div className="absolute top-9 left-0 w-full bg-vsc-sidebar border border-vsc-border rounded shadow-xl z-50 p-1 flex flex-col max-h-64 animate-in fade-in slide-in-from-top-1 duration-150">
|
|
210
|
+
<div className="overflow-y-auto flex-1">
|
|
211
|
+
{menuResults.length === 0 ? (
|
|
212
|
+
<div className="p-4 text-center text-[11px] text-vsc-muted italic">
|
|
213
|
+
No indexed record entities match your text query constraints.
|
|
214
|
+
</div>
|
|
215
|
+
) : (
|
|
216
|
+
menuResults.map((item) => (
|
|
217
|
+
<div
|
|
218
|
+
key={item.id}
|
|
219
|
+
onClick={() => {
|
|
220
|
+
if (onResultClick) onResultClick(item);
|
|
221
|
+
setIsMenuOpen(false);
|
|
222
|
+
}}
|
|
223
|
+
className="w-full p-2 hover:bg-vsc-hover text-left rounded-sm cursor-pointer flex flex-col space-y-0.5 transition-colors"
|
|
224
|
+
>
|
|
225
|
+
<div className="font-semibold text-vsc-text flex justify-between items-center">
|
|
226
|
+
<span className="truncate">{item.title}</span>
|
|
227
|
+
{item.category && (
|
|
228
|
+
<span className="text-[9px] font-mono font-bold bg-vsc-accent/10 border border-vsc-accent/20 text-vsc-accent px-1 rounded-sm uppercase tracking-wide">
|
|
229
|
+
{item.category}
|
|
230
|
+
</span>
|
|
231
|
+
)}
|
|
232
|
+
</div>
|
|
233
|
+
{item.subtitle && <div className="text-[10px] text-vsc-muted truncate">{item.subtitle}</div>}
|
|
234
|
+
</div>
|
|
235
|
+
))
|
|
236
|
+
)}
|
|
237
|
+
</div>
|
|
238
|
+
|
|
239
|
+
{menuResults.length > 0 && onViewMoreClick && (
|
|
240
|
+
<button
|
|
241
|
+
onClick={() => {
|
|
242
|
+
onViewMoreClick();
|
|
243
|
+
setIsMenuOpen(false);
|
|
244
|
+
}}
|
|
245
|
+
className="w-full border-t border-vsc-border bg-vsc-bg hover:bg-vsc-hover/60 p-2 text-center text-[10px] font-bold text-vsc-accent tracking-wide uppercase transition-colors rounded-b-sm border-none cursor-pointer"
|
|
246
|
+
>
|
|
247
|
+
View All Matching Search Metrics Records →
|
|
248
|
+
</button>
|
|
249
|
+
)}
|
|
250
|
+
</div>
|
|
251
|
+
)}
|
|
252
|
+
</div>
|
|
253
|
+
);
|
|
254
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type SearchVariant = 'simple' | 'float' | 'menu';
|
|
2
|
+
|
|
3
|
+
export interface SearchResultItem {
|
|
4
|
+
id: string;
|
|
5
|
+
title: string;
|
|
6
|
+
subtitle?: string;
|
|
7
|
+
category?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface SearchInputProps {
|
|
11
|
+
variant?: SearchVariant;
|
|
12
|
+
value: string;
|
|
13
|
+
onChange: (value: string) => void;
|
|
14
|
+
onClear?: () => void;
|
|
15
|
+
placeholder?: string;
|
|
16
|
+
resultsCount?: number; // Mandatory for 'simple' variant display logs
|
|
17
|
+
menuResults?: SearchResultItem[]; // Array datasets parsed directly to the 'menu' view dropdown
|
|
18
|
+
onResultClick?: (item: SearchResultItem) => void;
|
|
19
|
+
onViewMoreClick?: () => void;
|
|
20
|
+
className?: string;
|
|
21
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useTab } from './useTab';
|
|
3
|
+
import { TabVariant } from './types';
|
|
4
|
+
|
|
5
|
+
interface TabButtonProps {
|
|
6
|
+
id: string;
|
|
7
|
+
scopeName: string;
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
variant?: TabVariant;
|
|
10
|
+
startIcon?: React.ReactNode;
|
|
11
|
+
endIcon?: React.ReactNode;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default function TabButton({
|
|
15
|
+
id,
|
|
16
|
+
scopeName,
|
|
17
|
+
children,
|
|
18
|
+
variant = 'underline',
|
|
19
|
+
startIcon,
|
|
20
|
+
endIcon
|
|
21
|
+
}: TabButtonProps) {
|
|
22
|
+
const { activeTab, changeTab } = useTab(scopeName);
|
|
23
|
+
const isCurrent = activeTab === id;
|
|
24
|
+
|
|
25
|
+
const baseStyles = "flex items-center justify-center gap-1.5 focus:outline-none border-none outline-none z-10 transition-colors select-none h-full";
|
|
26
|
+
|
|
27
|
+
const variantClasses: Record<TabVariant, string> = {
|
|
28
|
+
underline: `pb-2 pt-1 px-3 text-xs font-semibold cursor-pointer border-b-2 ${
|
|
29
|
+
isCurrent ? 'border-vsc-accent text-vsc-text' : 'border-transparent text-vsc-muted hover:text-vsc-text'
|
|
30
|
+
}`,
|
|
31
|
+
'sliding-underline': `py-2 px-4 text-xs font-semibold cursor-pointer transition-colors duration-200 ${
|
|
32
|
+
isCurrent ? 'text-vsc-text font-bold' : 'text-vsc-muted hover:text-vsc-text'
|
|
33
|
+
}`,
|
|
34
|
+
pill: `px-3.5 py-1.5 text-xs font-bold rounded-full cursor-pointer scale-100 ${
|
|
35
|
+
isCurrent ? 'bg-vsc-accent text-vsc-button-text shadow-sm' : 'bg-vsc-hover/40 text-vsc-muted hover:bg-vsc-hover'
|
|
36
|
+
}`,
|
|
37
|
+
vscode: `px-4 text-xs font-medium border-r border-vsc-border cursor-pointer ${
|
|
38
|
+
isCurrent ? 'bg-vsc-bg text-vsc-text border-t-2 border-t-vsc-accent -mt-[1px]' : 'bg-vsc-sidebar text-vsc-muted hover:bg-vsc-hover/50'
|
|
39
|
+
}`,
|
|
40
|
+
ghost: `px-3 py-1.5 text-xs font-semibold rounded cursor-pointer ${
|
|
41
|
+
isCurrent ? 'bg-vsc-hover text-vsc-text font-bold' : 'text-vsc-muted hover:bg-vsc-hover/30'
|
|
42
|
+
}`
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<button
|
|
47
|
+
data-id={id}
|
|
48
|
+
onClick={() => changeTab(id)}
|
|
49
|
+
className={`${baseStyles} ${variantClasses[variant]}`}
|
|
50
|
+
>
|
|
51
|
+
{startIcon && <span className="shrink-0 opacity-80">{startIcon}</span>}
|
|
52
|
+
<span className="truncate">{children}</span>
|
|
53
|
+
{endIcon && <span className="shrink-0 opacity-70">{endIcon}</span>}
|
|
54
|
+
</button>
|
|
55
|
+
);
|
|
56
|
+
}
|