@object-ui/components 0.5.0 → 3.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 (119) hide show
  1. package/.turbo/turbo-build.log +12 -25
  2. package/CHANGELOG.md +32 -0
  3. package/dist/index.css +1 -1
  4. package/dist/index.js +23987 -22576
  5. package/dist/index.umd.cjs +30 -30
  6. package/dist/src/custom/action-param-dialog.d.ts +21 -0
  7. package/dist/src/custom/index.d.ts +4 -0
  8. package/dist/src/custom/navigation-overlay.d.ts +50 -0
  9. package/dist/src/custom/view-skeleton.d.ts +37 -0
  10. package/dist/src/custom/view-states.d.ts +33 -0
  11. package/dist/src/index.d.ts +1 -0
  12. package/dist/src/renderers/action/action-button.d.ts +11 -0
  13. package/dist/src/renderers/action/action-group.d.ts +25 -0
  14. package/dist/src/renderers/action/action-icon.d.ts +10 -0
  15. package/dist/src/renderers/action/action-menu.d.ts +19 -0
  16. package/dist/src/renderers/action/index.d.ts +0 -0
  17. package/dist/src/renderers/action/resolve-icon.d.ts +6 -0
  18. package/package.json +20 -19
  19. package/src/__tests__/PageRendererRegions.test.tsx +664 -55
  20. package/src/__tests__/__snapshots__/snapshot-critical.test.tsx.snap +811 -0
  21. package/src/__tests__/__snapshots__/snapshot.test.tsx.snap +327 -0
  22. package/src/__tests__/accessibility.test.tsx +137 -0
  23. package/src/__tests__/api-consistency.test.tsx +596 -0
  24. package/src/__tests__/color-contrast.test.tsx +212 -0
  25. package/src/__tests__/compliance.test.tsx +72 -0
  26. package/src/__tests__/edge-cases.test.tsx +285 -0
  27. package/src/__tests__/navigation-overlay.test.tsx +273 -0
  28. package/src/__tests__/snapshot-critical.test.tsx +317 -0
  29. package/src/__tests__/snapshot.test.tsx +205 -0
  30. package/src/__tests__/view-compliance.test.tsx +153 -0
  31. package/src/__tests__/wcag-audit.test.tsx +493 -0
  32. package/src/custom/action-param-dialog.tsx +264 -0
  33. package/src/custom/index.ts +4 -0
  34. package/src/custom/navigation-overlay.tsx +296 -0
  35. package/src/custom/view-skeleton.tsx +243 -0
  36. package/src/custom/view-states.tsx +153 -0
  37. package/src/index.ts +1 -0
  38. package/src/renderers/action/action-button.tsx +147 -0
  39. package/src/renderers/action/action-group.tsx +270 -0
  40. package/src/renderers/action/action-icon.tsx +150 -0
  41. package/src/renderers/action/action-menu.tsx +203 -0
  42. package/src/renderers/action/index.ts +18 -0
  43. package/src/renderers/action/resolve-icon.ts +35 -0
  44. package/src/renderers/complex/__tests__/data-table-batch-editing.test.tsx +275 -0
  45. package/src/renderers/complex/__tests__/data-table-cell-renderer.test.tsx +120 -0
  46. package/src/renderers/complex/__tests__/data-table-editing.test.tsx +221 -0
  47. package/src/renderers/complex/data-table.tsx +269 -33
  48. package/src/renderers/complex/resizable.tsx +20 -17
  49. package/src/renderers/data-display/list.tsx +1 -1
  50. package/src/renderers/data-display/table.tsx +1 -1
  51. package/src/renderers/data-display/tree-view.tsx +2 -1
  52. package/src/renderers/form/form.tsx +33 -10
  53. package/src/renderers/index.ts +1 -0
  54. package/src/renderers/layout/aspect-ratio.tsx +1 -1
  55. package/src/renderers/layout/page.tsx +416 -52
  56. package/src/renderers/navigation/sidebar.tsx +6 -0
  57. package/src/renderers/placeholders.tsx +2 -2
  58. package/src/stories/MockedData.stories.tsx +87 -37
  59. package/src/stories-json/Accessibility.mdx +297 -0
  60. package/src/stories-json/EdgeCases.stories.tsx +160 -0
  61. package/src/stories-json/GettingStarted.mdx +89 -0
  62. package/src/stories-json/Introduction.mdx +127 -0
  63. package/src/stories-json/accordion.stories.tsx +1 -1
  64. package/src/stories-json/aggrid.stories.tsx +1 -1
  65. package/src/stories-json/alert.stories.tsx +1 -1
  66. package/src/stories-json/aspect-ratio.stories.tsx +1 -1
  67. package/src/stories-json/avatar.stories.tsx +1 -1
  68. package/src/stories-json/badge.stories.tsx +1 -1
  69. package/src/stories-json/breadcrumb.stories.tsx +1 -1
  70. package/src/stories-json/button-group.stories.tsx +1 -1
  71. package/src/stories-json/button.stories.tsx +1 -1
  72. package/src/stories-json/calendar.stories.tsx +1 -1
  73. package/src/stories-json/card.stories.tsx +1 -1
  74. package/src/stories-json/carousel.stories.tsx +1 -1
  75. package/src/stories-json/charts.stories.tsx +1 -1
  76. package/src/stories-json/chatbot.stories.tsx +1 -1
  77. package/src/stories-json/code-editor.stories.tsx +1 -1
  78. package/src/stories-json/collapsible.stories.tsx +1 -1
  79. package/src/stories-json/controls.stories.tsx +1 -1
  80. package/src/stories-json/crm-live-data.stories.tsx +154 -0
  81. package/src/stories-json/data-table.stories.tsx +80 -4
  82. package/src/stories-json/data_display_extras.stories.tsx +1 -1
  83. package/src/stories-json/date-picker.stories.tsx +1 -1
  84. package/src/stories-json/detail-view.stories.tsx +1 -1
  85. package/src/stories-json/dialog.stories.tsx +1 -1
  86. package/src/stories-json/feedback_extras.stories.tsx +1 -1
  87. package/src/stories-json/feedback_others.stories.tsx +1 -1
  88. package/src/stories-json/form-variants.stories.tsx +210 -0
  89. package/src/stories-json/form_advanced.stories.tsx +1 -1
  90. package/src/stories-json/form_extras.stories.tsx +1 -1
  91. package/src/stories-json/grid.stories.tsx +1 -1
  92. package/src/stories-json/icon.stories.tsx +1 -1
  93. package/src/stories-json/input.stories.tsx +1 -1
  94. package/src/stories-json/kanban.stories.tsx +1 -1
  95. package/src/stories-json/layout_extended.stories.tsx +1 -1
  96. package/src/stories-json/layout_flex.stories.tsx +1 -1
  97. package/src/stories-json/list-view.stories.tsx +1 -1
  98. package/src/stories-json/markdown.stories.tsx +1 -1
  99. package/src/stories-json/menus.stories.tsx +1 -1
  100. package/src/stories-json/metric-card.stories.tsx +1 -1
  101. package/src/stories-json/navigation-menu.stories.tsx +1 -1
  102. package/src/stories-json/object-aggrid-advanced.stories.tsx +389 -0
  103. package/src/stories-json/object-aggrid.stories.tsx +1 -1
  104. package/src/stories-json/object-form.stories.tsx +1 -1
  105. package/src/stories-json/object-gantt.stories.tsx +1 -1
  106. package/src/stories-json/object-grid.stories.tsx +159 -1
  107. package/src/stories-json/object-map.stories.tsx +1 -1
  108. package/src/stories-json/object-view.stories.tsx +1 -1
  109. package/src/stories-json/overlay_extras.stories.tsx +1 -1
  110. package/src/stories-json/overlay_others.stories.tsx +1 -1
  111. package/src/stories-json/resizable.stories.tsx +1 -1
  112. package/src/stories-json/select.stories.tsx +1 -1
  113. package/src/stories-json/separator.stories.tsx +1 -1
  114. package/src/stories-json/statistic.stories.tsx +1 -1
  115. package/src/stories-json/tabs.stories.tsx +1 -1
  116. package/src/stories-json/timeline.stories.tsx +1 -1
  117. package/src/stories-json/typography.stories.tsx +1 -1
  118. package/src/ui/slider.tsx +6 -2
  119. package/src/stories/Introduction.mdx +0 -34
@@ -0,0 +1,264 @@
1
+ /**
2
+ * ObjectUI — Action Param Dialog
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ *
8
+ * Renders a dialog to collect ActionParam values before action execution.
9
+ * Used by the ActionRunner when an action defines params to collect.
10
+ */
11
+
12
+ import React, { useState, useCallback } from 'react';
13
+ import type { ActionParamDef } from '@object-ui/core';
14
+ import {
15
+ Dialog,
16
+ DialogContent,
17
+ DialogDescription,
18
+ DialogFooter,
19
+ DialogHeader,
20
+ DialogTitle,
21
+ } from '../ui/dialog';
22
+ import { Button } from '../ui/button';
23
+ import { Input } from '../ui/input';
24
+ import { Label } from '../ui/label';
25
+ import { Textarea } from '../ui/textarea';
26
+ import { Checkbox } from '../ui/checkbox';
27
+ import {
28
+ Select,
29
+ SelectContent,
30
+ SelectItem,
31
+ SelectTrigger,
32
+ SelectValue,
33
+ } from '../ui/select';
34
+
35
+ export interface ActionParamDialogProps {
36
+ /** The param definitions to render */
37
+ params: ActionParamDef[];
38
+ /** Whether the dialog is open */
39
+ open: boolean;
40
+ /** Called when the user submits the form */
41
+ onSubmit: (values: Record<string, any>) => void;
42
+ /** Called when the user cancels */
43
+ onCancel: () => void;
44
+ /** Dialog title */
45
+ title?: string;
46
+ /** Dialog description */
47
+ description?: string;
48
+ }
49
+
50
+ /**
51
+ * ActionParamDialog renders a dialog with form fields for each ActionParam.
52
+ * It collects user input and returns the values on submit.
53
+ */
54
+ export const ActionParamDialog: React.FC<ActionParamDialogProps> = ({
55
+ params,
56
+ open,
57
+ onSubmit,
58
+ onCancel,
59
+ title = 'Action Parameters',
60
+ description = 'Please provide the required parameters.',
61
+ }) => {
62
+ // Initialize values from defaultValues
63
+ const [values, setValues] = useState<Record<string, any>>(() => {
64
+ const initial: Record<string, any> = {};
65
+ params.forEach((p) => {
66
+ if (p.defaultValue !== undefined) {
67
+ initial[p.name] = p.defaultValue;
68
+ } else {
69
+ initial[p.name] = p.type === 'boolean' ? false : '';
70
+ }
71
+ });
72
+ return initial;
73
+ });
74
+
75
+ const [errors, setErrors] = useState<Record<string, string>>({});
76
+
77
+ const handleChange = useCallback((name: string, value: any) => {
78
+ setValues((prev) => ({ ...prev, [name]: value }));
79
+ // Clear error on change
80
+ setErrors((prev) => {
81
+ const next = { ...prev };
82
+ delete next[name];
83
+ return next;
84
+ });
85
+ }, []);
86
+
87
+ const handleSubmit = useCallback(() => {
88
+ // Validate required fields
89
+ const newErrors: Record<string, string> = {};
90
+ params.forEach((p) => {
91
+ if (p.required) {
92
+ const val = values[p.name];
93
+ if (val === undefined || val === null || val === '') {
94
+ newErrors[p.name] = `${p.label} is required`;
95
+ }
96
+ }
97
+ });
98
+
99
+ if (Object.keys(newErrors).length > 0) {
100
+ setErrors(newErrors);
101
+ return;
102
+ }
103
+
104
+ onSubmit(values);
105
+ }, [params, values, onSubmit]);
106
+
107
+ const renderField = (param: ActionParamDef) => {
108
+ const value = values[param.name];
109
+ const error = errors[param.name];
110
+
111
+ switch (param.type) {
112
+ case 'textarea':
113
+ return (
114
+ <div key={param.name} className="space-y-2">
115
+ <Label htmlFor={param.name}>
116
+ {param.label}
117
+ {param.required && <span className="text-destructive ml-1">*</span>}
118
+ </Label>
119
+ <Textarea
120
+ id={param.name}
121
+ value={value || ''}
122
+ onChange={(e) => handleChange(param.name, e.target.value)}
123
+ placeholder={param.placeholder}
124
+ />
125
+ {param.helpText && (
126
+ <p className="text-sm text-muted-foreground">{param.helpText}</p>
127
+ )}
128
+ {error && <p className="text-sm text-destructive">{error}</p>}
129
+ </div>
130
+ );
131
+
132
+ case 'number':
133
+ return (
134
+ <div key={param.name} className="space-y-2">
135
+ <Label htmlFor={param.name}>
136
+ {param.label}
137
+ {param.required && <span className="text-destructive ml-1">*</span>}
138
+ </Label>
139
+ <Input
140
+ id={param.name}
141
+ type="number"
142
+ value={value ?? ''}
143
+ onChange={(e) => handleChange(param.name, Number.isNaN(e.target.valueAsNumber) ? '' : e.target.valueAsNumber)}
144
+ placeholder={param.placeholder}
145
+ />
146
+ {param.helpText && (
147
+ <p className="text-sm text-muted-foreground">{param.helpText}</p>
148
+ )}
149
+ {error && <p className="text-sm text-destructive">{error}</p>}
150
+ </div>
151
+ );
152
+
153
+ case 'boolean':
154
+ return (
155
+ <div key={param.name} className="flex items-center space-x-2">
156
+ <Checkbox
157
+ id={param.name}
158
+ checked={!!value}
159
+ onCheckedChange={(checked) => handleChange(param.name, !!checked)}
160
+ />
161
+ <Label htmlFor={param.name}>{param.label}</Label>
162
+ {param.helpText && (
163
+ <p className="text-sm text-muted-foreground ml-2">{param.helpText}</p>
164
+ )}
165
+ </div>
166
+ );
167
+
168
+ case 'select':
169
+ return (
170
+ <div key={param.name} className="space-y-2">
171
+ <Label htmlFor={param.name}>
172
+ {param.label}
173
+ {param.required && <span className="text-destructive ml-1">*</span>}
174
+ </Label>
175
+ <Select
176
+ value={value || ''}
177
+ onValueChange={(val) => handleChange(param.name, val)}
178
+ >
179
+ <SelectTrigger>
180
+ <SelectValue placeholder={param.placeholder || 'Select...'} />
181
+ </SelectTrigger>
182
+ <SelectContent>
183
+ {param.options?.map((opt) => (
184
+ <SelectItem key={opt.value} value={opt.value}>
185
+ {opt.label}
186
+ </SelectItem>
187
+ ))}
188
+ </SelectContent>
189
+ </Select>
190
+ {param.helpText && (
191
+ <p className="text-sm text-muted-foreground">{param.helpText}</p>
192
+ )}
193
+ {error && <p className="text-sm text-destructive">{error}</p>}
194
+ </div>
195
+ );
196
+
197
+ case 'date':
198
+ return (
199
+ <div key={param.name} className="space-y-2">
200
+ <Label htmlFor={param.name}>
201
+ {param.label}
202
+ {param.required && <span className="text-destructive ml-1">*</span>}
203
+ </Label>
204
+ <Input
205
+ id={param.name}
206
+ type="date"
207
+ value={value || ''}
208
+ onChange={(e) => handleChange(param.name, e.target.value)}
209
+ />
210
+ {param.helpText && (
211
+ <p className="text-sm text-muted-foreground">{param.helpText}</p>
212
+ )}
213
+ {error && <p className="text-sm text-destructive">{error}</p>}
214
+ </div>
215
+ );
216
+
217
+ // text and all other types default to text input
218
+ default:
219
+ return (
220
+ <div key={param.name} className="space-y-2">
221
+ <Label htmlFor={param.name}>
222
+ {param.label}
223
+ {param.required && <span className="text-destructive ml-1">*</span>}
224
+ </Label>
225
+ <Input
226
+ id={param.name}
227
+ type="text"
228
+ value={value || ''}
229
+ onChange={(e) => handleChange(param.name, e.target.value)}
230
+ placeholder={param.placeholder}
231
+ />
232
+ {param.helpText && (
233
+ <p className="text-sm text-muted-foreground">{param.helpText}</p>
234
+ )}
235
+ {error && <p className="text-sm text-destructive">{error}</p>}
236
+ </div>
237
+ );
238
+ }
239
+ };
240
+
241
+ return (
242
+ <Dialog open={open} onOpenChange={(isOpen) => { if (!isOpen) onCancel(); }}>
243
+ <DialogContent className="sm:max-w-[425px]">
244
+ <DialogHeader>
245
+ <DialogTitle>{title}</DialogTitle>
246
+ <DialogDescription>{description}</DialogDescription>
247
+ </DialogHeader>
248
+ <div className="space-y-4 py-4">
249
+ {params.map(renderField)}
250
+ </div>
251
+ <DialogFooter>
252
+ <Button variant="outline" onClick={onCancel}>
253
+ Cancel
254
+ </Button>
255
+ <Button onClick={handleSubmit}>
256
+ Continue
257
+ </Button>
258
+ </DialogFooter>
259
+ </DialogContent>
260
+ </Dialog>
261
+ );
262
+ };
263
+
264
+ ActionParamDialog.displayName = 'ActionParamDialog';
@@ -8,5 +8,9 @@ export * from './input-group';
8
8
  export * from './item';
9
9
  export * from './kbd';
10
10
  export * from './native-select';
11
+ export * from './navigation-overlay';
11
12
  export * from './spinner';
12
13
  export * from './sort-builder';
14
+ export * from './action-param-dialog';
15
+ export * from './view-skeleton';
16
+ export * from './view-states';
@@ -0,0 +1,296 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+
9
+ /**
10
+ * NavigationOverlay
11
+ *
12
+ * A reusable component that renders record detail overlays based on
13
+ * ViewNavigationConfig mode. Supports drawer (Sheet), modal (Dialog),
14
+ * split (ResizablePanelGroup), and popover modes.
15
+ *
16
+ * Works in conjunction with useNavigationOverlay hook from @object-ui/react —
17
+ * the hook manages state while this component handles the visual presentation.
18
+ *
19
+ * @example
20
+ * ```tsx
21
+ * import { useNavigationOverlay } from '@object-ui/react';
22
+ * import { NavigationOverlay } from '@object-ui/components';
23
+ *
24
+ * const nav = useNavigationOverlay({
25
+ * navigation: schema.navigation,
26
+ * objectName: schema.objectName,
27
+ * });
28
+ *
29
+ * return (
30
+ * <>
31
+ * <DataTable onRowClick={nav.handleClick} />
32
+ * <NavigationOverlay {...nav} title="Record Detail">
33
+ * {(record) => <RecordDetail record={record} />}
34
+ * </NavigationOverlay>
35
+ * </>
36
+ * );
37
+ * ```
38
+ */
39
+
40
+ import React from 'react';
41
+ import { cn } from '../lib/utils';
42
+ import {
43
+ Sheet,
44
+ SheetContent,
45
+ SheetHeader,
46
+ SheetTitle,
47
+ SheetDescription,
48
+ } from '../ui/sheet';
49
+ import {
50
+ Dialog,
51
+ DialogContent,
52
+ DialogHeader,
53
+ DialogTitle,
54
+ DialogDescription,
55
+ } from '../ui/dialog';
56
+ import {
57
+ Popover,
58
+ PopoverContent,
59
+ PopoverTrigger,
60
+ } from '../ui/popover';
61
+ import {
62
+ ResizablePanelGroup,
63
+ ResizablePanel,
64
+ ResizableHandle,
65
+ } from '../ui/resizable';
66
+
67
+ /** Navigation mode type — matches ViewNavigationConfig.mode */
68
+ export type NavigationOverlayMode =
69
+ | 'page'
70
+ | 'drawer'
71
+ | 'modal'
72
+ | 'split'
73
+ | 'popover'
74
+ | 'new_window'
75
+ | 'none';
76
+
77
+ export interface NavigationOverlayProps {
78
+ /** Whether the overlay is open */
79
+ isOpen: boolean;
80
+ /** The selected record */
81
+ selectedRecord: Record<string, unknown> | null;
82
+ /** The navigation mode */
83
+ mode: NavigationOverlayMode;
84
+ /** Close the overlay */
85
+ close: () => void;
86
+ /** Set open state (for controlled Sheet/Dialog onOpenChange) */
87
+ setIsOpen: (open: boolean) => void;
88
+ /** Width for the overlay (drawer/modal/split) */
89
+ width?: string | number;
90
+ /** Whether navigation is an overlay mode */
91
+ isOverlay: boolean;
92
+ /** Title for the overlay header */
93
+ title?: string;
94
+ /** Description for the overlay header */
95
+ description?: string;
96
+ /** CSS class for the overlay container */
97
+ className?: string;
98
+ /**
99
+ * Render function for the overlay content.
100
+ * Receives the selected record.
101
+ */
102
+ children: (record: Record<string, unknown>) => React.ReactNode;
103
+ /**
104
+ * The main content to wrap (for split mode only).
105
+ * In split mode, the main content is rendered in the left panel.
106
+ */
107
+ mainContent?: React.ReactNode;
108
+ /**
109
+ * Popover trigger element (for popover mode).
110
+ */
111
+ popoverTrigger?: React.ReactNode;
112
+ }
113
+
114
+ /**
115
+ * Resolve width to CSS-compatible value
116
+ */
117
+ function resolveWidth(width: string | number | undefined): string | undefined {
118
+ if (width == null) return undefined;
119
+ if (typeof width === 'number') return `${width}px`;
120
+ return width;
121
+ }
122
+
123
+ /**
124
+ * Compute CSS style from NavigationConfig width
125
+ */
126
+ function getWidthStyle(width: string | number | undefined): React.CSSProperties {
127
+ const resolved = resolveWidth(width);
128
+ if (!resolved) return {};
129
+ return { maxWidth: resolved, width: '100%' };
130
+ }
131
+
132
+ /**
133
+ * NavigationOverlay — renders record detail in the configured overlay mode.
134
+ *
135
+ * Supports:
136
+ * - **drawer**: Right-side Sheet panel
137
+ * - **modal**: Center Dialog overlay
138
+ * - **split**: Side-by-side ResizablePanelGroup
139
+ * - **popover**: Hoverable/clickable popover card
140
+ * - **page / new_window / none**: No overlay rendered (handled by hook)
141
+ */
142
+ export const NavigationOverlay: React.FC<NavigationOverlayProps> = ({
143
+ isOpen,
144
+ selectedRecord,
145
+ mode,
146
+ close,
147
+ setIsOpen,
148
+ width,
149
+ title,
150
+ description,
151
+ className,
152
+ children,
153
+ mainContent,
154
+ popoverTrigger,
155
+ }) => {
156
+ // Non-overlay modes don't render anything
157
+ if (mode === 'page' || mode === 'new_window' || mode === 'none') {
158
+ return null;
159
+ }
160
+
161
+ if (!selectedRecord) {
162
+ return null;
163
+ }
164
+
165
+ const widthStyle = getWidthStyle(width);
166
+ const resolvedTitle = title || 'Record Detail';
167
+
168
+ // --- Drawer Mode (Sheet) ---
169
+ if (mode === 'drawer') {
170
+ return (
171
+ <Sheet open={isOpen} onOpenChange={setIsOpen}>
172
+ <SheetContent
173
+ side="right"
174
+ className={cn('w-full sm:max-w-2xl overflow-y-auto', className)}
175
+ style={widthStyle}
176
+ >
177
+ <SheetHeader>
178
+ <SheetTitle>{resolvedTitle}</SheetTitle>
179
+ {description && <SheetDescription>{description}</SheetDescription>}
180
+ </SheetHeader>
181
+ <div className="mt-4">
182
+ {children(selectedRecord)}
183
+ </div>
184
+ </SheetContent>
185
+ </Sheet>
186
+ );
187
+ }
188
+
189
+ // --- Modal Mode (Dialog) ---
190
+ if (mode === 'modal') {
191
+ return (
192
+ <Dialog open={isOpen} onOpenChange={setIsOpen}>
193
+ <DialogContent
194
+ className={cn('max-w-2xl max-h-[90vh] overflow-y-auto', className)}
195
+ style={widthStyle}
196
+ >
197
+ <DialogHeader>
198
+ <DialogTitle>{resolvedTitle}</DialogTitle>
199
+ {description && <DialogDescription>{description}</DialogDescription>}
200
+ </DialogHeader>
201
+ <div className="mt-4">
202
+ {children(selectedRecord)}
203
+ </div>
204
+ </DialogContent>
205
+ </Dialog>
206
+ );
207
+ }
208
+
209
+ // --- Split Mode (Resizable Panels) ---
210
+ if (mode === 'split') {
211
+ if (!isOpen || !mainContent) {
212
+ return null;
213
+ }
214
+
215
+ // Calculate panel sizes based on width config
216
+ const detailPercent = width
217
+ ? typeof width === 'number'
218
+ ? Math.min(70, Math.max(20, (width / 1200) * 100))
219
+ : 40
220
+ : 40;
221
+ const mainPercent = 100 - detailPercent;
222
+
223
+ // Cast needed: ResizablePanelGroup has correct runtime behavior but
224
+ // vite-plugin-dts may not resolve the direction prop type correctly
225
+ const PanelGroup = ResizablePanelGroup as React.FC<any>;
226
+
227
+ return (
228
+ <PanelGroup direction="horizontal" className={cn('h-full', className)}>
229
+ <ResizablePanel defaultSize={mainPercent} minSize={30}>
230
+ {mainContent}
231
+ </ResizablePanel>
232
+ <ResizableHandle withHandle />
233
+ <ResizablePanel defaultSize={detailPercent} minSize={20}>
234
+ <div className="h-full overflow-y-auto p-4">
235
+ <div className="flex items-center justify-between mb-4">
236
+ <h3 className="text-lg font-semibold">{resolvedTitle}</h3>
237
+ <button
238
+ onClick={close}
239
+ className="rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
240
+ aria-label="Close panel"
241
+ >
242
+ <svg
243
+ xmlns="http://www.w3.org/2000/svg"
244
+ width="16"
245
+ height="16"
246
+ viewBox="0 0 24 24"
247
+ fill="none"
248
+ stroke="currentColor"
249
+ strokeWidth="2"
250
+ strokeLinecap="round"
251
+ strokeLinejoin="round"
252
+ >
253
+ <line x1="18" y1="6" x2="6" y2="18" />
254
+ <line x1="6" y1="6" x2="18" y2="18" />
255
+ </svg>
256
+ </button>
257
+ </div>
258
+ {description && (
259
+ <p className="text-sm text-muted-foreground mb-4">{description}</p>
260
+ )}
261
+ {children(selectedRecord)}
262
+ </div>
263
+ </ResizablePanel>
264
+ </PanelGroup>
265
+ );
266
+ }
267
+
268
+ // --- Popover Mode ---
269
+ if (mode === 'popover') {
270
+ return (
271
+ <Popover open={isOpen} onOpenChange={setIsOpen}>
272
+ {popoverTrigger && (
273
+ <PopoverTrigger asChild>
274
+ {popoverTrigger}
275
+ </PopoverTrigger>
276
+ )}
277
+ <PopoverContent
278
+ className={cn('w-96 max-h-[400px] overflow-y-auto p-4', className)}
279
+ style={widthStyle}
280
+ >
281
+ <div className="space-y-2">
282
+ <h4 className="text-sm font-semibold">{resolvedTitle}</h4>
283
+ {description && (
284
+ <p className="text-xs text-muted-foreground">{description}</p>
285
+ )}
286
+ {children(selectedRecord)}
287
+ </div>
288
+ </PopoverContent>
289
+ </Popover>
290
+ );
291
+ }
292
+
293
+ return null;
294
+ };
295
+
296
+ NavigationOverlay.displayName = 'NavigationOverlay';