@aiquants/select-box 0.1.0 → 0.2.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/README.md +100 -321
- package/dist/index.cjs +6 -6
- package/dist/index.js +375 -896
- package/dist/select-box.css +1 -1
- package/dist/src/components/DropdownList.d.ts.map +1 -1
- package/dist/src/components/InputArea.d.ts +4 -0
- package/dist/src/components/InputArea.d.ts.map +1 -1
- package/dist/src/components/SelectBox.d.ts.map +1 -1
- package/dist/src/hooks/useSearchSort.d.ts +4 -1
- package/dist/src/hooks/useSearchSort.d.ts.map +1 -1
- package/dist/src/hooks/useSelectBox.d.ts +1 -0
- package/dist/src/hooks/useSelectBox.d.ts.map +1 -1
- package/dist/src/types/index.d.ts +6 -0
- package/dist/src/types/index.d.ts.map +1 -1
- package/dist/styles/select-box.css +2 -2
- package/dist/vite.config.d.ts.map +1 -1
- package/package.json +34 -35
package/README.md
CHANGED
|
@@ -1,375 +1,154 @@
|
|
|
1
|
-
# @aiquants/
|
|
1
|
+
# @aiquants/select-box
|
|
2
2
|
|
|
3
|
-
A high-performance React
|
|
3
|
+
A high-performance React select box component with virtual scrolling, fuzzy search, and multi-selection support.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
- **💾 State Persistence**: Automatic localStorage persistence for expansion states
|
|
15
|
-
- **🎯 Flexible Selection**: Support for none, single, or multiple selection modes
|
|
7
|
+
- **Virtual Scrolling**: Built with `@aiquants/virtualscroll` to handle 1,000+ options smoothly
|
|
8
|
+
- **Fuzzy Search**: Built with `@aiquants/fuzzy-search` using Web Workers for non-blocking search
|
|
9
|
+
- **Single & Multi Select**: Unified API for both modes via the `multiple` prop
|
|
10
|
+
- **Clearable**: Optional clear button for single-select mode
|
|
11
|
+
- **Resizable Dropdown**: Drag the bottom-right handle to resize (100px - 600px)
|
|
12
|
+
- **Keyboard Navigation**: Backspace to remove last tag, external click to close
|
|
13
|
+
- **TypeScript**: Complete type definitions included
|
|
16
14
|
|
|
17
15
|
## Installation
|
|
18
16
|
|
|
19
17
|
```bash
|
|
20
|
-
|
|
21
|
-
# or
|
|
22
|
-
yarn add @aiquants/directory-tree
|
|
23
|
-
# or
|
|
24
|
-
pnpm add @aiquants/directory-tree
|
|
18
|
+
pnpm add @aiquants/select-box
|
|
25
19
|
```
|
|
26
20
|
|
|
27
21
|
## Peer Dependencies
|
|
28
22
|
|
|
29
|
-
This package requires the following peer dependencies:
|
|
30
|
-
|
|
31
23
|
```bash
|
|
32
|
-
|
|
24
|
+
pnpm add react react-dom
|
|
33
25
|
```
|
|
34
26
|
|
|
35
27
|
## Quick Start
|
|
36
28
|
|
|
37
29
|
```tsx
|
|
38
|
-
import
|
|
39
|
-
import {
|
|
40
|
-
|
|
41
|
-
import
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
absolutePath: '/src/components/App.tsx',
|
|
57
|
-
relativePath: 'src/components/App.tsx',
|
|
58
|
-
children: null
|
|
59
|
-
}
|
|
60
|
-
]
|
|
61
|
-
}
|
|
62
|
-
]
|
|
63
|
-
}
|
|
64
|
-
];
|
|
65
|
-
|
|
66
|
-
export default function App() {
|
|
67
|
-
const { theme } = useTheme();
|
|
68
|
-
const {
|
|
69
|
-
toggle,
|
|
70
|
-
isExpanded,
|
|
71
|
-
expandMultiple,
|
|
72
|
-
collapseMultiple,
|
|
73
|
-
isPending
|
|
74
|
-
} = useDirectoryTreeState({
|
|
75
|
-
storageKey: 'my-directory-tree'
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
// Calculate line color based on theme
|
|
79
|
-
const lineColor = theme === "dark" ? "#4A5568" : "#A0AEC0";
|
|
80
|
-
|
|
81
|
-
const handleFileSelect = (absolutePath: string, relativePath: string) => {
|
|
82
|
-
console.log(`File selected: ${absolutePath} (${relativePath})`);
|
|
83
|
-
};
|
|
30
|
+
import { useState, useMemo } from "react"
|
|
31
|
+
import { SelectBox, type Option } from "@aiquants/select-box"
|
|
32
|
+
// Required: Vite environments need explicit Worker URLs
|
|
33
|
+
import indexWorkerUrl from "@aiquants/fuzzy-search/worker/indexWorker?url"
|
|
34
|
+
import levenshteinWorkerUrl from "@aiquants/fuzzy-search/worker/levenshteinWorker?url"
|
|
35
|
+
|
|
36
|
+
const options: Option[] = [
|
|
37
|
+
{ label: "Apple", value: "apple" },
|
|
38
|
+
{ label: "Banana", value: "banana" },
|
|
39
|
+
{ label: "Cherry", value: "cherry" },
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
function SingleSelect() {
|
|
43
|
+
const [value, setValue] = useState<Option | null>(null)
|
|
44
|
+
const workerUrls = useMemo(() => ({
|
|
45
|
+
indexWorker: indexWorkerUrl,
|
|
46
|
+
levenshteinWorker: levenshteinWorkerUrl,
|
|
47
|
+
}), [])
|
|
84
48
|
|
|
85
49
|
return (
|
|
86
|
-
<
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
isPending
|
|
95
|
-
}}
|
|
96
|
-
selection={{
|
|
97
|
-
onFileSelect: handleFileSelect,
|
|
98
|
-
selectedPath: null
|
|
99
|
-
}}
|
|
100
|
-
visual={{
|
|
101
|
-
lineColor,
|
|
102
|
-
className: "h-full"
|
|
103
|
-
}}
|
|
104
|
-
/>
|
|
105
|
-
</div>
|
|
106
|
-
);
|
|
50
|
+
<SelectBox
|
|
51
|
+
options={options}
|
|
52
|
+
value={value}
|
|
53
|
+
onChange={(val) => setValue(val as Option | null)}
|
|
54
|
+
placeholder="Select a fruit..."
|
|
55
|
+
workerUrls={workerUrls}
|
|
56
|
+
/>
|
|
57
|
+
)
|
|
107
58
|
}
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
## API Reference
|
|
111
|
-
|
|
112
|
-
### DirectoryTree Component
|
|
113
|
-
|
|
114
|
-
The main component for rendering the directory tree.
|
|
115
|
-
|
|
116
|
-
#### Props
|
|
117
|
-
|
|
118
|
-
| Prop | Type | Required | Description |
|
|
119
|
-
|------|------|----------|-------------|
|
|
120
|
-
| `entries` | `DirectoryEntry[]` | Yes | Array of root directory entries to display |
|
|
121
|
-
| `expansion` | `object` | Yes | Configuration for tree expansion state and behavior |
|
|
122
|
-
| `selection` | `object` | Yes | Configuration for item selection |
|
|
123
|
-
| `visual` | `object` | No | Visual customization options |
|
|
124
|
-
| `virtualScroll` | `DirectoryTreeVirtualScrollOptions` | No | Pass-through options for the underlying VirtualScroll component |
|
|
125
|
-
|
|
126
|
-
#### Expansion Options (`expansion`)
|
|
127
|
-
|
|
128
|
-
| Prop | Type | Required | Default | Description |
|
|
129
|
-
|------|------|----------|---------|-------------|
|
|
130
|
-
| `toggle` | `(path: string, relativePath: string) => void` | Yes | - | Function to toggle directory expansion state |
|
|
131
|
-
| `isExpanded` | `(path: string) => boolean` | Yes | - | Function to check if a directory is expanded |
|
|
132
|
-
| `expandMultiple` | `(paths: string[]) => void` | Yes | - | Function to expand multiple directories |
|
|
133
|
-
| `collapseMultiple` | `(paths: string[]) => void` | Yes | - | Function to collapse multiple directories |
|
|
134
|
-
| `isPending` | `boolean` | No | `false` | Whether the tree is in a pending state |
|
|
135
|
-
| `alwaysExpanded` | `boolean` | No | `false` | If true, all directories are always expanded |
|
|
136
|
-
| `doubleClickAction` | `'recursive' \| 'toggle'` | No | `'recursive'` | Action on double-clicking a directory |
|
|
137
|
-
|
|
138
|
-
#### Selection Options (`selection`)
|
|
139
|
-
|
|
140
|
-
| Prop | Type | Required | Default | Description |
|
|
141
|
-
|------|------|----------|---------|-------------|
|
|
142
|
-
| `onFileSelect` | `(absolutePath: string, relativePath: string) => void` | Yes | - | Callback function triggered when a file is selected |
|
|
143
|
-
| `selectedPath` | `string \| null` | No | - | The currently selected file path |
|
|
144
|
-
| `mode` | `'none' \| 'single' \| 'multiple'` | No | `'none'` | Selection mode for items |
|
|
145
|
-
| `selectedItems` | `Set<string>` | No | - | Set of paths for currently selected items |
|
|
146
|
-
| `onSelectionChange` | `(path: string, isSelected: boolean) => void` | No | - | Callback when item selection changes |
|
|
147
|
-
|
|
148
|
-
#### Visual Options (`visual`)
|
|
149
59
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
| `showExpandIcons` | `boolean` | No | `true` | Flag indicating whether to render directory expand icons |
|
|
157
|
-
| `showDirectoryIcons` | `boolean` | No | `true` | Flag indicating whether to render directory type icons |
|
|
158
|
-
| `showFileIcons` | `boolean` | No | `true` | Flag indicating whether to render file type icons |
|
|
159
|
-
| `iconOverrides` | `DirectoryTreeIconOverrides` | No | - | Icon overrides applied globally |
|
|
160
|
-
| `expandIconSize` | `number` | No | - | Size of the expand icon |
|
|
161
|
-
| `removeRootIndent` | `boolean` | No | `false` | If true, removes the indentation and connector lines for root-level items |
|
|
162
|
-
|
|
163
|
-
### Virtual Scroll Options
|
|
164
|
-
|
|
165
|
-
`virtualScroll` lets you customize the embedded `@aiquants/virtualscroll` instance without re-implementing list rendering. Every option is optional and mirrors the VirtualScroll API.
|
|
166
|
-
|
|
167
|
-
- `overscanCount`: Adjust how many items render beyond the viewport for smoother scrolling (default: `10`).
|
|
168
|
-
- `tapScrollCircleOptions`: Forward tap circle customization such as radius or visual renderer.
|
|
169
|
-
- `scrollBarWidth`: Override the custom scrollbar width.
|
|
170
|
-
- `enableThumbDrag`, `enableTrackClick`, `enableArrowButtons`, `enablePointerDrag`: Toggle individual scrollbar interactions.
|
|
171
|
-
- `inertiaOptions`: Fine-tune inertial scrolling parameters.
|
|
172
|
-
- `onScroll`, `onRangeChange`, `className`, `background`, `initialScrollIndex`, `initialScrollOffset`: Hook into scroll lifecycle or provide bespoke styling.
|
|
173
|
-
|
|
174
|
-
Example:
|
|
175
|
-
|
|
176
|
-
```tsx
|
|
177
|
-
<DirectoryTree
|
|
178
|
-
{...commonProps}
|
|
179
|
-
virtualScroll={{
|
|
180
|
-
overscanCount: 6,
|
|
181
|
-
enablePointerDrag: false,
|
|
182
|
-
scrollBarWidth: 14,
|
|
183
|
-
tapScrollCircleOptions: {
|
|
184
|
-
radius: 32
|
|
185
|
-
}
|
|
186
|
-
}}
|
|
187
|
-
/>;
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
### useDirectoryTreeState Hook
|
|
191
|
-
|
|
192
|
-
A hook for managing directory tree state with localStorage persistence.
|
|
193
|
-
|
|
194
|
-
#### Parameters
|
|
195
|
-
|
|
196
|
-
| Parameter | Type | Description |
|
|
197
|
-
|-----------|------|-------------|
|
|
198
|
-
| `options` | `UseDirectoryTreeStateProps` | Configuration options |
|
|
199
|
-
|
|
200
|
-
#### Options
|
|
201
|
-
|
|
202
|
-
| Option | Type | Description |
|
|
203
|
-
|--------|------|-------------|
|
|
204
|
-
| `initialExpanded` | `Set<string>` | Initially expanded directories |
|
|
205
|
-
| `storageKey` | `string` | localStorage key for persistence (default: 'directory-tree-state') |
|
|
206
|
-
|
|
207
|
-
#### Returns
|
|
208
|
-
|
|
209
|
-
| Property | Type | Description |
|
|
210
|
-
|----------|------|-------------|
|
|
211
|
-
| `expanded` | `Set<string>` | Currently expanded directories |
|
|
212
|
-
| `toggle` | `(path: string, relativePath: string) => void` | Toggle directory expansion |
|
|
213
|
-
| `isExpanded` | `(path: string) => boolean` | Check if directory is expanded |
|
|
214
|
-
| `expand` | `(path: string) => void` | Expand a directory |
|
|
215
|
-
| `collapse` | `(path: string) => void` | Collapse a directory |
|
|
216
|
-
| `expandMultiple` | `(paths: string[]) => void` | Expand multiple directories |
|
|
217
|
-
| `collapseMultiple` | `(paths: string[]) => void` | Collapse multiple directories |
|
|
218
|
-
| `collapseAll` | `() => void` | Collapse all directories |
|
|
219
|
-
| `isPending` | `boolean` | Whether a transition is pending |
|
|
220
|
-
|
|
221
|
-
### DirectoryEntry Type
|
|
222
|
-
|
|
223
|
-
```typescript
|
|
224
|
-
type DirectoryEntry = {
|
|
225
|
-
name: string;
|
|
226
|
-
absolutePath: string;
|
|
227
|
-
relativePath: string;
|
|
228
|
-
children: DirectoryEntry[] | null;
|
|
229
|
-
};
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
## Styling
|
|
233
|
-
|
|
234
|
-
The component uses Tailwind CSS for styling. Make sure you have Tailwind CSS configured in your project. The component supports both light and dark themes, but theme control is managed by the calling component.
|
|
235
|
-
|
|
236
|
-
### Theme Control
|
|
237
|
-
|
|
238
|
-
The `lineColor` prop allows you to control the tree line color based on your application's theme:
|
|
239
|
-
|
|
240
|
-
```tsx
|
|
241
|
-
import { useTheme } from './hooks/useTheme';
|
|
242
|
-
|
|
243
|
-
function MyComponent() {
|
|
244
|
-
const { theme } = useTheme();
|
|
245
|
-
|
|
246
|
-
// Calculate line color based on theme
|
|
247
|
-
const lineColor = theme === "dark" ? "#4A5568" : "#A0AEC0";
|
|
60
|
+
function MultiSelect() {
|
|
61
|
+
const [values, setValues] = useState<Option[]>([])
|
|
62
|
+
const workerUrls = useMemo(() => ({
|
|
63
|
+
indexWorker: indexWorkerUrl,
|
|
64
|
+
levenshteinWorker: levenshteinWorkerUrl,
|
|
65
|
+
}), [])
|
|
248
66
|
|
|
249
67
|
return (
|
|
250
|
-
<
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
68
|
+
<SelectBox
|
|
69
|
+
options={options}
|
|
70
|
+
value={values}
|
|
71
|
+
onChange={(val) => setValues(Array.isArray(val) ? val : [])}
|
|
72
|
+
multiple
|
|
73
|
+
placeholder="Select multiple..."
|
|
74
|
+
workerUrls={workerUrls}
|
|
255
75
|
/>
|
|
256
|
-
)
|
|
76
|
+
)
|
|
257
77
|
}
|
|
258
78
|
```
|
|
259
79
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
You can customize the appearance by overriding the default Tailwind classes:
|
|
80
|
+
## CSS Setup
|
|
263
81
|
|
|
264
|
-
|
|
265
|
-
<DirectoryTree
|
|
266
|
-
visual={{
|
|
267
|
-
className: "custom-directory-tree",
|
|
268
|
-
style: { height: '400px' }
|
|
269
|
-
}}
|
|
270
|
-
// ... other props
|
|
271
|
-
/>
|
|
272
|
-
```
|
|
82
|
+
Import the CSS in your application's stylesheet:
|
|
273
83
|
|
|
274
84
|
```css
|
|
275
|
-
|
|
276
|
-
/* Your custom styles */
|
|
277
|
-
}
|
|
85
|
+
@import "@aiquants/select-box/styles/select-box.css" layer(components);
|
|
278
86
|
```
|
|
279
87
|
|
|
280
|
-
##
|
|
88
|
+
## API Reference
|
|
281
89
|
|
|
282
|
-
###
|
|
90
|
+
### SelectBox Props
|
|
283
91
|
|
|
284
|
-
|
|
92
|
+
| Prop | Type | Required | Default | Description |
|
|
93
|
+
|------|------|----------|---------|-------------|
|
|
94
|
+
| `options` | `Option[]` | Yes | - | Array of selectable options |
|
|
95
|
+
| `value` | `Option \| Option[]` | No | `undefined` | Current selection (controlled) |
|
|
96
|
+
| `onChange` | `(value: Option \| Option[] \| null) => void` | No | `undefined` | Selection change callback |
|
|
97
|
+
| `placeholder` | `string` | No | `"Select..."` | Placeholder text when nothing is selected |
|
|
98
|
+
| `multiple` | `boolean` | No | `false` | Enable multi-select mode |
|
|
99
|
+
| `className` | `string` | No | `""` | Additional CSS class for the container |
|
|
100
|
+
| `disabled` | `boolean` | No | `false` | Disable the component |
|
|
101
|
+
| `clearable` | `boolean` | No | `false` | Show a clear button (single-select) |
|
|
102
|
+
| `autoFocus` | `boolean` | No | `false` | Auto-focus the input on mount |
|
|
103
|
+
| `workerUrls` | `{ indexWorker?: string; levenshteinWorker?: string }` | No | `undefined` | Explicit Web Worker URLs (see below) |
|
|
104
|
+
|
|
105
|
+
### Option Type
|
|
285
106
|
|
|
286
|
-
```
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
// ... other required props
|
|
293
|
-
visual={{
|
|
294
|
-
style: { height: '600px' }
|
|
295
|
-
}}
|
|
296
|
-
/>
|
|
107
|
+
```typescript
|
|
108
|
+
interface Option {
|
|
109
|
+
label: string // Display text (search target)
|
|
110
|
+
value: string | number // Unique identifier
|
|
111
|
+
[key: string]: any // Custom metadata
|
|
112
|
+
}
|
|
297
113
|
```
|
|
298
114
|
|
|
299
|
-
|
|
115
|
+
## Web Worker URLs (Important)
|
|
300
116
|
|
|
301
|
-
|
|
117
|
+
The fuzzy search uses Web Workers internally. In **Vite development mode**, the default `import.meta.url`-based Worker resolution may fail, causing errors like:
|
|
302
118
|
|
|
303
|
-
```
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
setSelectedItems(prev => {
|
|
308
|
-
const newSet = new Set(prev);
|
|
309
|
-
if (isSelected) {
|
|
310
|
-
newSet.add(path);
|
|
311
|
-
} else {
|
|
312
|
-
newSet.delete(path);
|
|
313
|
-
}
|
|
314
|
-
return newSet;
|
|
315
|
-
});
|
|
316
|
-
};
|
|
317
|
-
|
|
318
|
-
<DirectoryTree
|
|
319
|
-
// ... other props
|
|
320
|
-
selection={{
|
|
321
|
-
mode: "multiple",
|
|
322
|
-
selectedItems: selectedItems,
|
|
323
|
-
onSelectionChange: handleSelectionChange,
|
|
324
|
-
onFileSelect: handleFileSelect // Required prop
|
|
325
|
-
}}
|
|
326
|
-
/>
|
|
119
|
+
```text
|
|
120
|
+
[fuzzy-search] Index Worker Error
|
|
121
|
+
[fuzzy-search] Index Worker: Ping timeout
|
|
122
|
+
[fuzzy-search] Failed to rebuild index: Error: Index build timeout
|
|
327
123
|
```
|
|
328
124
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
Control how directories behave on double-click:
|
|
125
|
+
To fix this, pass `workerUrls` with Vite's `?url` import:
|
|
332
126
|
|
|
333
127
|
```tsx
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
128
|
+
import indexWorkerUrl from "@aiquants/fuzzy-search/worker/indexWorker?url"
|
|
129
|
+
import levenshteinWorkerUrl from "@aiquants/fuzzy-search/worker/levenshteinWorker?url"
|
|
130
|
+
|
|
131
|
+
<SelectBox
|
|
132
|
+
options={options}
|
|
133
|
+
workerUrls={{
|
|
134
|
+
indexWorker: indexWorkerUrl,
|
|
135
|
+
levenshteinWorker: levenshteinWorkerUrl,
|
|
341
136
|
}}
|
|
342
137
|
/>
|
|
343
138
|
```
|
|
344
139
|
|
|
345
|
-
|
|
140
|
+
## Dependencies
|
|
346
141
|
|
|
347
|
-
|
|
142
|
+
| Package | Purpose |
|
|
143
|
+
|---------|---------|
|
|
144
|
+
| `@aiquants/fuzzy-search` | Web Worker-based fuzzy search engine |
|
|
145
|
+
| `@aiquants/virtualscroll` | Virtual scrolling for large lists |
|
|
146
|
+
| `tailwind-merge` | Tailwind CSS class conflict resolution |
|
|
348
147
|
|
|
349
|
-
|
|
350
|
-
const { toggle, isExpanded, expandMultiple, collapseMultiple } = useDirectoryTreeState({
|
|
351
|
-
storageKey: 'myapp-directory-tree',
|
|
352
|
-
initialExpanded: new Set(['/src', '/docs'])
|
|
353
|
-
});
|
|
354
|
-
```
|
|
355
|
-
|
|
356
|
-
## TypeScript Support
|
|
357
|
-
|
|
358
|
-
This package is written in TypeScript and provides comprehensive type definitions. All components and hooks are fully typed for the best development experience.
|
|
359
|
-
|
|
360
|
-
## Contributing
|
|
148
|
+
## Documentation
|
|
361
149
|
|
|
362
|
-
|
|
150
|
+
See the [docs/specs/](docs/specs/) directory for detailed specifications.
|
|
363
151
|
|
|
364
152
|
## License
|
|
365
153
|
|
|
366
|
-
MIT
|
|
367
|
-
|
|
368
|
-
## Author
|
|
369
|
-
|
|
370
|
-
- **GitHub**: [fehde-k](https://github.com/fehde-k)
|
|
371
|
-
- **X (formerly Twitter)**: [@fehdek](https://x.com/fehdek)
|
|
372
|
-
|
|
373
|
-
---
|
|
374
|
-
|
|
375
|
-
Made with ❤️ by the AIQuants team.
|
|
154
|
+
MIT
|