@optilogic/core 1.0.0-beta.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 (70) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +107 -0
  3. package/dist/index.cjs +6003 -0
  4. package/dist/index.cjs.map +1 -0
  5. package/dist/index.d.cts +2310 -0
  6. package/dist/index.d.ts +2310 -0
  7. package/dist/index.js +5828 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/styles.css +96 -0
  10. package/dist/tailwind-preset.cjs +106 -0
  11. package/dist/tailwind-preset.cjs.map +1 -0
  12. package/dist/tailwind-preset.d.cts +23 -0
  13. package/dist/tailwind-preset.d.ts +23 -0
  14. package/dist/tailwind-preset.js +101 -0
  15. package/dist/tailwind-preset.js.map +1 -0
  16. package/package.json +154 -0
  17. package/src/components/accordion.tsx +187 -0
  18. package/src/components/alert-dialog.tsx +143 -0
  19. package/src/components/autocomplete.tsx +271 -0
  20. package/src/components/badge.tsx +62 -0
  21. package/src/components/button.tsx +85 -0
  22. package/src/components/calendar.tsx +235 -0
  23. package/src/components/card.tsx +94 -0
  24. package/src/components/checkbox.tsx +77 -0
  25. package/src/components/chip.tsx +77 -0
  26. package/src/components/confirmation-modal.tsx +195 -0
  27. package/src/components/context-menu.tsx +406 -0
  28. package/src/components/copy-button.tsx +84 -0
  29. package/src/components/data-grid/DataGrid.tsx +1027 -0
  30. package/src/components/data-grid/components/CellEditor.tsx +346 -0
  31. package/src/components/data-grid/components/FilterPopover.tsx +459 -0
  32. package/src/components/data-grid/components/HeaderCell.tsx +207 -0
  33. package/src/components/data-grid/components/index.ts +14 -0
  34. package/src/components/data-grid/hooks/index.ts +28 -0
  35. package/src/components/data-grid/hooks/useColumnResize.ts +378 -0
  36. package/src/components/data-grid/hooks/useDataGridState.ts +346 -0
  37. package/src/components/data-grid/hooks/useKeyboardNavigation.ts +361 -0
  38. package/src/components/data-grid/index.ts +71 -0
  39. package/src/components/data-grid/types.ts +478 -0
  40. package/src/components/data-grid/utils/dataProcessing.ts +277 -0
  41. package/src/components/data-grid/utils/index.ts +12 -0
  42. package/src/components/date-picker.tsx +366 -0
  43. package/src/components/dropdown-menu.tsx +230 -0
  44. package/src/components/icon-button.tsx +157 -0
  45. package/src/components/input.tsx +40 -0
  46. package/src/components/label.tsx +37 -0
  47. package/src/components/loading-spinner.tsx +113 -0
  48. package/src/components/modal.tsx +207 -0
  49. package/src/components/popover.tsx +62 -0
  50. package/src/components/progress.tsx +41 -0
  51. package/src/components/resizable-panel.tsx +434 -0
  52. package/src/components/resize-handle.tsx +187 -0
  53. package/src/components/select.tsx +160 -0
  54. package/src/components/separator.tsx +50 -0
  55. package/src/components/skeleton.tsx +37 -0
  56. package/src/components/switch.tsx +59 -0
  57. package/src/components/table.tsx +136 -0
  58. package/src/components/tabs.tsx +102 -0
  59. package/src/components/textarea.tsx +36 -0
  60. package/src/components/theme-picker.tsx +245 -0
  61. package/src/components/toaster.tsx +84 -0
  62. package/src/components/tooltip.tsx +199 -0
  63. package/src/index.ts +318 -0
  64. package/src/styles.css +96 -0
  65. package/src/tailwind-preset.ts +129 -0
  66. package/src/theme/index.ts +41 -0
  67. package/src/theme/presets.ts +502 -0
  68. package/src/theme/types.ts +164 -0
  69. package/src/theme/utils.ts +309 -0
  70. package/src/utils/cn.ts +14 -0
@@ -0,0 +1,434 @@
1
+ import * as React from "react";
2
+
3
+ import { cn } from "../utils/cn";
4
+ import { ResizeHandle } from "./resize-handle";
5
+
6
+ export interface ResizablePanelProps {
7
+ /**
8
+ * Which edge the panel anchors to
9
+ */
10
+ orientation: "left" | "right";
11
+
12
+ /**
13
+ * Default collapsed width in vw
14
+ */
15
+ collapsedSizeVW: number;
16
+
17
+ /**
18
+ * Current expanded width in vw (controlled prop)
19
+ */
20
+ expandedWidthVW?: number;
21
+
22
+ /**
23
+ * Minimum width in vw
24
+ */
25
+ minWidthVW?: number;
26
+
27
+ /**
28
+ * Maximum width in vw
29
+ */
30
+ maxWidthVW?: number;
31
+
32
+ /**
33
+ * Whether the panel is expanded (drawer) vs collapsed
34
+ */
35
+ isExpanded: boolean;
36
+
37
+ /**
38
+ * Whether the panel is in overlay mode
39
+ */
40
+ isOverlay?: boolean;
41
+
42
+ /**
43
+ * When in overlay mode, the outer panel width in vw
44
+ */
45
+ outerWidthVW?: number;
46
+
47
+ /**
48
+ * When in overlay mode, the inner panel width in vw
49
+ */
50
+ innerWidthVW?: number;
51
+
52
+ /**
53
+ * Current left panel width in vw (needed for overlay positioning and dynamic constraints)
54
+ */
55
+ leftWidthVW?: number;
56
+
57
+ /**
58
+ * Whether the panel is resizable
59
+ */
60
+ resizable?: boolean;
61
+
62
+ /**
63
+ * Show/hide the resize handle
64
+ */
65
+ showHandle?: boolean;
66
+
67
+ /**
68
+ * Callback during resize; emits clamped value in vw
69
+ */
70
+ onResize?: (widthVW: number) => void;
71
+
72
+ /**
73
+ * Callback for collapse/expand toggle
74
+ */
75
+ onToggle?: (next: boolean) => void;
76
+
77
+ /**
78
+ * Callback to promote to overlay mode
79
+ */
80
+ onPromoteToOverlay?: () => void;
81
+
82
+ /**
83
+ * Callback to demote from overlay mode
84
+ */
85
+ onDemoteFromOverlay?: () => void;
86
+
87
+ /**
88
+ * Callback when resizing the overlay outer/inner split
89
+ */
90
+ onResizeOverlay?: (outerWidthVW: number) => void;
91
+
92
+ /**
93
+ * ARIA label for the resize handle
94
+ */
95
+ handleAriaLabel?: string;
96
+
97
+ /**
98
+ * Optional header slot
99
+ */
100
+ slotHeader?: React.ReactNode;
101
+
102
+ /**
103
+ * Optional footer slot
104
+ */
105
+ slotFooter?: React.ReactNode;
106
+
107
+ /**
108
+ * Optional inner panel content (for overlay mode)
109
+ */
110
+ innerPanelContent?: React.ReactNode;
111
+
112
+ /**
113
+ * Panel content
114
+ */
115
+ children: React.ReactNode;
116
+
117
+ /**
118
+ * Additional class names
119
+ */
120
+ className?: string;
121
+
122
+ /**
123
+ * Data attributes for styling/state
124
+ */
125
+ dataAttributes?: Record<string, string>;
126
+ }
127
+
128
+ /**
129
+ * ResizablePanel component
130
+ *
131
+ * A generic, framework-agnostic panel that can collapse, expand, resize,
132
+ * and promote to overlay mode. Backs both left (search) and right (preview) panels.
133
+ */
134
+ export function ResizablePanel({
135
+ orientation,
136
+ collapsedSizeVW,
137
+ expandedWidthVW,
138
+ minWidthVW = 5,
139
+ maxWidthVW = 90,
140
+ isExpanded,
141
+ isOverlay = false,
142
+ outerWidthVW,
143
+ innerWidthVW,
144
+ leftWidthVW,
145
+ resizable = true,
146
+ showHandle = true,
147
+ onResize,
148
+ onToggle: _onToggle,
149
+ onPromoteToOverlay: _onPromoteToOverlay,
150
+ onDemoteFromOverlay,
151
+ onResizeOverlay,
152
+ handleAriaLabel,
153
+ slotHeader,
154
+ slotFooter,
155
+ innerPanelContent,
156
+ children,
157
+ className,
158
+ dataAttributes = {},
159
+ }: ResizablePanelProps) {
160
+ const panelRef = React.useRef<HTMLDivElement>(null);
161
+
162
+ const [isDragging, setIsDragging] = React.useState(false);
163
+ const [localWidth, setLocalWidth] = React.useState<number | null>(null);
164
+ const [localOuterWidth, setLocalOuterWidth] = React.useState<number | null>(null);
165
+
166
+ const currentWidthVW = isExpanded
167
+ ? (isDragging && localWidth !== null ? localWidth : expandedWidthVW) ||
168
+ collapsedSizeVW
169
+ : collapsedSizeVW;
170
+
171
+ const clampWidth = React.useCallback(
172
+ (width: number): number => {
173
+ let effectiveMinWidth = minWidthVW;
174
+ let effectiveMaxWidth = maxWidthVW;
175
+
176
+ if (orientation === "right" && leftWidthVW !== undefined && !isOverlay) {
177
+ const availableSpace = 100 - leftWidthVW - 3;
178
+ const maxFromSpace = Math.max(effectiveMinWidth, availableSpace - 33);
179
+ effectiveMaxWidth = Math.min(maxWidthVW, maxFromSpace);
180
+ }
181
+
182
+ return Math.max(effectiveMinWidth, Math.min(effectiveMaxWidth, width));
183
+ },
184
+ [minWidthVW, maxWidthVW, orientation, leftWidthVW, isOverlay]
185
+ );
186
+
187
+ const handleDragStart = React.useCallback(() => {
188
+ setIsDragging(true);
189
+ setLocalWidth(currentWidthVW);
190
+ }, [currentWidthVW]);
191
+
192
+ const handleDrag = React.useCallback(
193
+ (deltaX: number) => {
194
+ if (!isExpanded || isOverlay) return;
195
+
196
+ const viewportWidth = window.innerWidth;
197
+ const deltaVW = (deltaX / viewportWidth) * 100;
198
+
199
+ const adjustedDelta = orientation === "left" ? deltaVW : -deltaVW;
200
+ const baseWidth = localWidth !== null ? localWidth : currentWidthVW;
201
+ const newWidth = clampWidth(baseWidth + adjustedDelta);
202
+
203
+ setLocalWidth(newWidth);
204
+ },
205
+ [
206
+ isExpanded,
207
+ isOverlay,
208
+ orientation,
209
+ localWidth,
210
+ currentWidthVW,
211
+ clampWidth,
212
+ ]
213
+ );
214
+
215
+ const handleDragEnd = React.useCallback(() => {
216
+ if (localWidth !== null && onResize) {
217
+ onResize(localWidth);
218
+ }
219
+ setIsDragging(false);
220
+ setLocalWidth(null);
221
+ }, [localWidth, onResize]);
222
+
223
+ const handleKeyboardResize = React.useCallback(
224
+ (direction: 1 | -1) => {
225
+ if (!isExpanded || isOverlay || !onResize) return;
226
+
227
+ const step = 1;
228
+ const newWidth = clampWidth(currentWidthVW + step * direction);
229
+ onResize(newWidth);
230
+ },
231
+ [isExpanded, isOverlay, onResize, currentWidthVW, clampWidth]
232
+ );
233
+
234
+ const handleOverlayDragStart = React.useCallback(() => {
235
+ if (outerWidthVW) {
236
+ setIsDragging(true);
237
+ setLocalOuterWidth(outerWidthVW);
238
+ }
239
+ }, [outerWidthVW]);
240
+
241
+ const handleOverlayDrag = React.useCallback(
242
+ (deltaX: number) => {
243
+ if (!isOverlay) return;
244
+
245
+ const viewportWidth = window.innerWidth;
246
+ const deltaVW = (deltaX / viewportWidth) * 100;
247
+
248
+ const baseWidth =
249
+ localOuterWidth !== null ? localOuterWidth : outerWidthVW || 30;
250
+ const newOuterWidth = baseWidth - deltaVW;
251
+
252
+ setLocalOuterWidth(newOuterWidth);
253
+ },
254
+ [isOverlay, localOuterWidth, outerWidthVW]
255
+ );
256
+
257
+ const handleOverlayDragEnd = React.useCallback(() => {
258
+ if (localOuterWidth !== null && onResizeOverlay) {
259
+ onResizeOverlay(localOuterWidth);
260
+ }
261
+ setIsDragging(false);
262
+ setLocalOuterWidth(null);
263
+ }, [localOuterWidth, onResizeOverlay]);
264
+
265
+ React.useEffect(() => {
266
+ if (isOverlay && panelRef.current) {
267
+ const focusableElement = panelRef.current.querySelector<HTMLElement>(
268
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
269
+ );
270
+ focusableElement?.focus();
271
+ }
272
+ }, [isOverlay]);
273
+
274
+ React.useEffect(() => {
275
+ if (!isOverlay) return;
276
+
277
+ const handleEscape = (e: KeyboardEvent) => {
278
+ if (e.key === "Escape") {
279
+ onDemoteFromOverlay?.();
280
+ }
281
+ };
282
+
283
+ window.addEventListener("keydown", handleEscape);
284
+ return () => window.removeEventListener("keydown", handleEscape);
285
+ }, [isOverlay, onDemoteFromOverlay]);
286
+
287
+ if (!isExpanded && !isOverlay) {
288
+ return (
289
+ <div
290
+ ref={panelRef}
291
+ className={cn(
292
+ "flex flex-col h-full",
293
+ "bg-background border-border",
294
+ orientation === "left" ? "border-r" : "border-l",
295
+ className
296
+ )}
297
+ style={{
298
+ width: `${collapsedSizeVW}vw`,
299
+ minWidth: "48px", // Ensure minimum pixel width to prevent squishing
300
+ }}
301
+ {...dataAttributes}
302
+ >
303
+ {children}
304
+ </div>
305
+ );
306
+ }
307
+
308
+ if (isOverlay && outerWidthVW && innerWidthVW) {
309
+ const displayOuterWidth =
310
+ isDragging && localOuterWidth !== null ? localOuterWidth : outerWidthVW;
311
+ const displayInnerWidth =
312
+ isDragging && localOuterWidth !== null
313
+ ? outerWidthVW + innerWidthVW - localOuterWidth
314
+ : innerWidthVW;
315
+
316
+ const overlayLeftPosition = leftWidthVW || 3;
317
+
318
+ return (
319
+ <div
320
+ ref={panelRef}
321
+ role="dialog"
322
+ aria-modal="true"
323
+ className={cn("fixed inset-y-0 z-30", "flex", className)}
324
+ style={{
325
+ left: `${overlayLeftPosition}vw`, // Start after left panel
326
+ right: `${3}vw`, // End before nav gutter (fixed right edge) - matches icon nav width
327
+ width: `${displayOuterWidth + displayInnerWidth}vw`,
328
+ }}
329
+ {...dataAttributes}
330
+ >
331
+ <div
332
+ className={cn(
333
+ "flex flex-col h-full",
334
+ "bg-background border-r border-border"
335
+ )}
336
+ style={{
337
+ width: `${displayInnerWidth}vw`,
338
+ }}
339
+ >
340
+ <div className="flex-1 overflow-auto p-4">
341
+ {innerPanelContent || (
342
+ <div className="text-muted-foreground text-sm">
343
+ Inner panel content area
344
+ </div>
345
+ )}
346
+ </div>
347
+ </div>
348
+
349
+ <ResizeHandle
350
+ orientation="right"
351
+ resizable={resizable}
352
+ showHandle={showHandle}
353
+ onDragStart={handleOverlayDragStart}
354
+ onDrag={handleOverlayDrag}
355
+ onDragEnd={handleOverlayDragEnd}
356
+ ariaLabel={handleAriaLabel || "Resize overlay panels"}
357
+ />
358
+
359
+ <div
360
+ className={cn("flex flex-col h-full", "bg-background border-border")}
361
+ style={{
362
+ width: `${displayOuterWidth}vw`,
363
+ }}
364
+ >
365
+ {slotHeader && (
366
+ <div className="flex-shrink-0 border-b border-border">
367
+ {slotHeader}
368
+ </div>
369
+ )}
370
+ <div className="flex-1 overflow-auto">{children}</div>
371
+ {slotFooter && (
372
+ <div className="flex-shrink-0 border-t border-border">
373
+ {slotFooter}
374
+ </div>
375
+ )}
376
+ </div>
377
+
378
+ <div
379
+ className="absolute inset-0 -z-10 bg-black/20"
380
+ onClick={() => onDemoteFromOverlay?.()}
381
+ />
382
+ </div>
383
+ );
384
+ }
385
+
386
+ return (
387
+ <div
388
+ ref={panelRef}
389
+ className={cn(
390
+ "relative flex h-full min-w-0 overflow-hidden",
391
+ !isDragging && "transition-[width] duration-200 ease-out",
392
+ orientation === "left" ? "flex-row" : "flex-row-reverse",
393
+ className
394
+ )}
395
+ style={{
396
+ width: `${currentWidthVW}vw`,
397
+ }}
398
+ {...dataAttributes}
399
+ >
400
+ <div
401
+ className={cn(
402
+ "flex flex-col flex-1 min-w-0 overflow-hidden",
403
+ "bg-background border-border",
404
+ orientation === "left" ? "border-r" : "border-l"
405
+ )}
406
+ >
407
+ {slotHeader && (
408
+ <div className="flex-shrink-0 border-b border-border min-w-0 overflow-hidden">
409
+ {slotHeader}
410
+ </div>
411
+ )}
412
+ <div className="flex-1 overflow-hidden min-w-0">{children}</div>
413
+ {slotFooter && (
414
+ <div className="flex-shrink-0 border-t border-border">
415
+ {slotFooter}
416
+ </div>
417
+ )}
418
+ </div>
419
+
420
+ {isExpanded && (
421
+ <ResizeHandle
422
+ orientation={orientation}
423
+ resizable={resizable}
424
+ showHandle={showHandle}
425
+ onDragStart={handleDragStart}
426
+ onDrag={handleDrag}
427
+ onDragEnd={handleDragEnd}
428
+ onKeyboardResize={handleKeyboardResize}
429
+ ariaLabel={handleAriaLabel}
430
+ />
431
+ )}
432
+ </div>
433
+ );
434
+ }
@@ -0,0 +1,187 @@
1
+ import * as React from "react";
2
+
3
+ import { cn } from "../utils/cn";
4
+
5
+ export interface ResizeHandleProps {
6
+ /**
7
+ * Orientation of the resize handle
8
+ */
9
+ orientation: "left" | "right";
10
+
11
+ /**
12
+ * Whether the handle is resizable (draggable)
13
+ */
14
+ resizable?: boolean;
15
+
16
+ /**
17
+ * Show/hide the handle UI
18
+ */
19
+ showHandle?: boolean;
20
+
21
+ /**
22
+ * Callback when drag starts
23
+ */
24
+ onDragStart?: () => void;
25
+
26
+ /**
27
+ * Callback during drag; receives pixel delta
28
+ */
29
+ onDrag?: (deltaX: number) => void;
30
+
31
+ /**
32
+ * Callback when drag ends
33
+ */
34
+ onDragEnd?: () => void;
35
+
36
+ /**
37
+ * Callback for keyboard resize (arrow keys)
38
+ */
39
+ onKeyboardResize?: (direction: 1 | -1) => void;
40
+
41
+ /**
42
+ * ARIA label for accessibility
43
+ */
44
+ ariaLabel?: string;
45
+
46
+ /**
47
+ * Additional class names
48
+ */
49
+ className?: string;
50
+ }
51
+
52
+ /**
53
+ * ResizeHandle component
54
+ *
55
+ * A draggable handle for resizing panels with full keyboard and mouse support.
56
+ * Includes visual feedback, accessibility features, and smooth interactions.
57
+ */
58
+ export function ResizeHandle({
59
+ orientation,
60
+ resizable = true,
61
+ showHandle = true,
62
+ onDragStart,
63
+ onDrag,
64
+ onDragEnd,
65
+ onKeyboardResize,
66
+ ariaLabel,
67
+ className,
68
+ }: ResizeHandleProps) {
69
+ const [isDragging, setIsDragging] = React.useState(false);
70
+ const [isFocused, setIsFocused] = React.useState(false);
71
+ const startXRef = React.useRef<number>(0);
72
+ const handleRef = React.useRef<HTMLDivElement>(null);
73
+
74
+ const handleMouseDown = React.useCallback(
75
+ (e: React.MouseEvent) => {
76
+ if (!resizable) return;
77
+
78
+ e.preventDefault();
79
+ setIsDragging(true);
80
+ startXRef.current = e.clientX;
81
+
82
+ // Prevent text selection during drag
83
+ document.body.style.userSelect = "none";
84
+
85
+ // Notify start
86
+ onDragStart?.();
87
+ },
88
+ [resizable, onDragStart]
89
+ );
90
+
91
+ const handleMouseMove = React.useCallback(
92
+ (e: MouseEvent) => {
93
+ if (!isDragging) return;
94
+
95
+ const deltaX = e.clientX - startXRef.current;
96
+ startXRef.current = e.clientX;
97
+
98
+ onDrag?.(deltaX);
99
+ },
100
+ [isDragging, onDrag]
101
+ );
102
+
103
+ const handleMouseUp = React.useCallback(() => {
104
+ if (!isDragging) return;
105
+
106
+ setIsDragging(false);
107
+ document.body.style.userSelect = "";
108
+ onDragEnd?.();
109
+ }, [isDragging, onDragEnd]);
110
+
111
+ const handleKeyDown = React.useCallback(
112
+ (e: React.KeyboardEvent) => {
113
+ if (!resizable || !onKeyboardResize) return;
114
+
115
+ let direction: 1 | -1 | null = null;
116
+
117
+ // Left arrow = shrink from right / expand from left
118
+ // Right arrow = expand from right / shrink from left
119
+ if (e.key === "ArrowLeft") {
120
+ direction = orientation === "right" ? -1 : 1;
121
+ } else if (e.key === "ArrowRight") {
122
+ direction = orientation === "right" ? 1 : -1;
123
+ }
124
+
125
+ if (direction !== null) {
126
+ e.preventDefault();
127
+ const multiplier = e.shiftKey ? 5 : 1; // Shift = 5vw steps
128
+ for (let i = 0; i < multiplier; i++) {
129
+ onKeyboardResize(direction);
130
+ }
131
+ }
132
+ },
133
+ [resizable, onKeyboardResize, orientation]
134
+ );
135
+
136
+ // Attach global mouse listeners during drag
137
+ React.useEffect(() => {
138
+ if (isDragging) {
139
+ window.addEventListener("mousemove", handleMouseMove);
140
+ window.addEventListener("mouseup", handleMouseUp);
141
+
142
+ return () => {
143
+ window.removeEventListener("mousemove", handleMouseMove);
144
+ window.removeEventListener("mouseup", handleMouseUp);
145
+ };
146
+ }
147
+ }, [isDragging, handleMouseMove, handleMouseUp]);
148
+
149
+ if (!showHandle) return null;
150
+
151
+ return (
152
+ <div
153
+ ref={handleRef}
154
+ role="separator"
155
+ aria-orientation="vertical"
156
+ aria-label={ariaLabel || `Resize handle ${orientation}`}
157
+ aria-valuenow={undefined}
158
+ tabIndex={resizable ? 0 : -1}
159
+ onMouseDown={handleMouseDown}
160
+ onKeyDown={handleKeyDown}
161
+ onFocus={() => setIsFocused(true)}
162
+ onBlur={() => setIsFocused(false)}
163
+ className={cn(
164
+ // Base styles
165
+ "relative z-10 flex items-center justify-center",
166
+ "transition-colors duration-150",
167
+
168
+ // Hit area (wider for better UX)
169
+ "w-3",
170
+
171
+ // Cursor
172
+ resizable ? "cursor-col-resize" : "cursor-default",
173
+
174
+ // Focus state - no visible outline
175
+ "outline-none",
176
+
177
+ // Disabled state
178
+ !resizable && "opacity-50",
179
+
180
+ className
181
+ )}
182
+ style={{
183
+ touchAction: "none",
184
+ }}
185
+ />
186
+ );
187
+ }