@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.
Files changed (115) hide show
  1. package/.storybook/main.ts +17 -0
  2. package/.storybook/preview.ts +23 -0
  3. package/debug-storybook.log +40 -0
  4. package/dist/chunks/jsx-runtime-Boo2vksn.js +182 -0
  5. package/dist/chunks/resizable-ImB8dfG_.js +112 -0
  6. package/dist/chunks/tabs-MaVN00hJ.js +86 -0
  7. package/dist/components/button/Button.d.ts +31 -0
  8. package/dist/components/button/ButtonGroup.d.ts +12 -0
  9. package/dist/components/button/SplitActionButton.d.ts +2 -0
  10. package/dist/components/button/index.d.ts +5 -0
  11. package/dist/components/button/split-types.d.ts +16 -0
  12. package/dist/components/button/types.d.ts +9 -0
  13. package/dist/components/icons/AddIcon.d.ts +2 -0
  14. package/dist/components/icons/BlankDocIcon.d.ts +2 -0
  15. package/dist/components/icons/ChatIcon.d.ts +2 -0
  16. package/dist/components/icons/ChevronDownIcon.d.ts +2 -0
  17. package/dist/components/icons/CloseIcon.d.ts +2 -0
  18. package/dist/components/icons/CommentIcon.d.ts +2 -0
  19. package/dist/components/icons/DeleteChatIcon.d.ts +2 -0
  20. package/dist/components/icons/DocumentIcon.d.ts +2 -0
  21. package/dist/components/icons/ExpandIcon.d.ts +2 -0
  22. package/dist/components/icons/FolderIcon.d.ts +2 -0
  23. package/dist/components/icons/GroupIcon.d.ts +2 -0
  24. package/dist/components/icons/MinimizeIcon.d.ts +2 -0
  25. package/dist/components/icons/MinusIcon.d.ts +2 -0
  26. package/dist/components/icons/MoreIcon.d.ts +2 -0
  27. package/dist/components/icons/OpenFolderIcon.d.ts +2 -0
  28. package/dist/components/icons/PersonIcon.d.ts +2 -0
  29. package/dist/components/icons/PlusChatIcon.d.ts +2 -0
  30. package/dist/components/icons/PlusCommentIcon.d.ts +2 -0
  31. package/dist/components/icons/PlusDocBadgeIcon.d.ts +8 -0
  32. package/dist/components/icons/PlusDocIcon.d.ts +2 -0
  33. package/dist/components/icons/PlusPersonIcon.d.ts +2 -0
  34. package/dist/components/icons/RefreshIcon.d.ts +2 -0
  35. package/dist/components/icons/SearchIcon.d.ts +2 -0
  36. package/dist/components/icons/TerminalIcon.d.ts +2 -0
  37. package/dist/components/icons/TrashIcon.d.ts +2 -0
  38. package/dist/components/icons/TruckIcon.d.ts +2 -0
  39. package/dist/components/icons/index.d.ts +26 -0
  40. package/dist/components/icons/types.d.ts +5 -0
  41. package/dist/components/resizable/ResizablePanel.d.ts +10 -0
  42. package/dist/components/resizable/index.d.ts +1 -0
  43. package/dist/components/resizable/index.js +2 -0
  44. package/dist/components/search/SearchInput.d.ts +10 -0
  45. package/dist/components/search/index.d.ts +2 -0
  46. package/dist/components/search/types.d.ts +19 -0
  47. package/dist/components/tabs/TabButton.d.ts +12 -0
  48. package/dist/components/tabs/TabButtonGroup.d.ts +9 -0
  49. package/dist/components/tabs/TabPanel.d.ts +8 -0
  50. package/dist/components/tabs/TabPanelList.d.ts +11 -0
  51. package/dist/components/tabs/index.d.ts +5 -0
  52. package/dist/components/tabs/index.js +2 -0
  53. package/dist/components/tabs/types.d.ts +9 -0
  54. package/dist/components/tabs/useTab.d.ts +5 -0
  55. package/dist/index.d.ts +5 -0
  56. package/dist/index.js +1071 -0
  57. package/package.json +68 -0
  58. package/src/components/button/Button.stories.tsx +70 -0
  59. package/src/components/button/Button.tsx +108 -0
  60. package/src/components/button/ButtonGroup.stories.tsx +63 -0
  61. package/src/components/button/ButtonGroup.tsx +62 -0
  62. package/src/components/button/SplitActionButton.tsx +116 -0
  63. package/src/components/button/SplitButton.stories.tsx +55 -0
  64. package/src/components/button/index.ts +7 -0
  65. package/src/components/button/split-types.ts +18 -0
  66. package/src/components/button/types.ts +10 -0
  67. package/src/components/icons/AddIcon.tsx +10 -0
  68. package/src/components/icons/BlankDocIcon.tsx +10 -0
  69. package/src/components/icons/ChatIcon.tsx +9 -0
  70. package/src/components/icons/ChevronDownIcon.tsx +9 -0
  71. package/src/components/icons/CloseIcon.tsx +23 -0
  72. package/src/components/icons/CommentIcon.tsx +9 -0
  73. package/src/components/icons/DeleteChatIcon.tsx +10 -0
  74. package/src/components/icons/DocumentIcon.tsx +13 -0
  75. package/src/components/icons/ExpandIcon.tsx +12 -0
  76. package/src/components/icons/FolderIcon.tsx +9 -0
  77. package/src/components/icons/GroupIcon.tsx +12 -0
  78. package/src/components/icons/Icon.stories.tsx +122 -0
  79. package/src/components/icons/MinimizeIcon.tsx +12 -0
  80. package/src/components/icons/MinusIcon.tsx +9 -0
  81. package/src/components/icons/MoreIcon.tsx +11 -0
  82. package/src/components/icons/OpenFolderIcon.tsx +37 -0
  83. package/src/components/icons/PersonIcon.tsx +10 -0
  84. package/src/components/icons/PlusChatIcon.tsx +11 -0
  85. package/src/components/icons/PlusCommentIcon.tsx +11 -0
  86. package/src/components/icons/PlusDocBadgeIcon.tsx +74 -0
  87. package/src/components/icons/PlusDocIcon.tsx +12 -0
  88. package/src/components/icons/PlusPersonIcon.tsx +12 -0
  89. package/src/components/icons/RefreshIcon.tsx +9 -0
  90. package/src/components/icons/SearchIcon.tsx +10 -0
  91. package/src/components/icons/TerminalIcon.tsx +11 -0
  92. package/src/components/icons/TrashIcon.tsx +12 -0
  93. package/src/components/icons/TruckIcon.tsx +12 -0
  94. package/src/components/icons/index.ts +26 -0
  95. package/src/components/icons/types.ts +6 -0
  96. package/src/components/resizable/ResizablePanel.tsx +183 -0
  97. package/src/components/resizable/index.ts +1 -0
  98. package/src/components/search/SearchInput.stories.tsx +91 -0
  99. package/src/components/search/SearchInput.tsx +254 -0
  100. package/src/components/search/index.ts +2 -0
  101. package/src/components/search/types.ts +21 -0
  102. package/src/components/tabs/TabButton.tsx +56 -0
  103. package/src/components/tabs/TabButtonGroup.tsx +82 -0
  104. package/src/components/tabs/TabPanel.tsx +44 -0
  105. package/src/components/tabs/TabPanelList.tsx +31 -0
  106. package/src/components/tabs/Tabs.stories.tsx +71 -0
  107. package/src/components/tabs/index.ts +5 -0
  108. package/src/components/tabs/types.ts +10 -0
  109. package/src/components/tabs/useTab.ts +33 -0
  110. package/src/index.css +35 -0
  111. package/src/index.ts +5 -0
  112. package/src/vite-env.d.ts +5 -0
  113. package/tsconfig.json +47 -0
  114. package/vite.config.ts +64 -0
  115. 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,2 @@
1
+ export { default as SearchInput } from './SearchInput';
2
+ export * from './types';
@@ -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
+ }