@object-ui/plugin-detail 3.1.5 → 3.3.1

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 (209) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +21 -1
  3. package/dist/AddressField-LgHnO2Lk.js +98 -0
  4. package/dist/AutoNumberField-xZCrU0eW.js +14 -0
  5. package/dist/{AvatarField-YGj51ozd.js → AvatarField-Dy2XGlPz.js} +16 -15
  6. package/dist/{BooleanField-CaA898Tk.js → BooleanField-C0Clfka5.js} +11 -10
  7. package/dist/CodeField-CHUa07B6.js +23 -0
  8. package/dist/ColorField-vxHqEhcS.js +38 -0
  9. package/dist/CurrencyField-DiWjYWDo.js +49 -0
  10. package/dist/DateField-DGaRPM4P.js +22 -0
  11. package/dist/DateTimeField-8QnpsI_h.js +30 -0
  12. package/dist/EmailField-CkVgMbpI.js +26 -0
  13. package/dist/FileField-5UPV7uek.js +149 -0
  14. package/dist/FormulaField-BUgt6-Pi.js +17 -0
  15. package/dist/GeolocationField-D9T_jgG6.js +118 -0
  16. package/dist/GridField-DE_HwiIN.js +49 -0
  17. package/dist/ImageField-Dswnqtzf.js +73 -0
  18. package/dist/LocationField-gjqbE6na.js +36 -0
  19. package/dist/LookupField-BcS3LRKc.js +901 -0
  20. package/dist/{MasterDetailField-I1A9oEGC.js → MasterDetailField-BF6_-X3A.js} +20 -19
  21. package/dist/NumberField-Dj2rYmrS.js +27 -0
  22. package/dist/ObjectField-BymIojwd.js +50 -0
  23. package/dist/{PasswordField-DBtluGJ1.js → PasswordField-ED_Xgqz-.js} +8 -7
  24. package/dist/PercentField-D-JKOxKC.js +61 -0
  25. package/dist/PhoneField-DSCaGYq7.js +26 -0
  26. package/dist/QRCodeField-CtcOUapi.js +73 -0
  27. package/dist/{RatingField-B_Mnr63i.js → RatingField-BDnyQFWy.js} +10 -9
  28. package/dist/RichTextField-CH6LVZQA.js +33 -0
  29. package/dist/SelectField-DE4dpkMV.js +36 -0
  30. package/dist/{SignatureField-CddhEK9u.js → SignatureField-B1wh3f5A.js} +18 -17
  31. package/dist/{SliderField-Df5hMzNc.js → SliderField-zoTCKh9n.js} +2 -1
  32. package/dist/SummaryField-BeBVT6VN.js +22 -0
  33. package/dist/TextAreaField-rfUGrRxh.js +37 -0
  34. package/dist/TextField-C_yM7ATQ.js +30 -0
  35. package/dist/TimeField-BcQmBZi9.js +22 -0
  36. package/dist/UrlField-BakaF6NI.js +31 -0
  37. package/dist/UserField-zS7y3eKb.js +76 -0
  38. package/dist/VectorField-CTZ4myDM.js +34 -0
  39. package/dist/index.d.ts +1 -1
  40. package/dist/index.js +1741 -1504
  41. package/dist/index.umd.cjs +43 -51
  42. package/dist/packages/plugin-detail/src/ActivityTimeline.d.ts.map +1 -0
  43. package/dist/packages/plugin-detail/src/CommentAttachment.d.ts.map +1 -0
  44. package/dist/packages/plugin-detail/src/CommentInput.d.ts.map +1 -0
  45. package/dist/packages/plugin-detail/src/DetailSection.d.ts.map +1 -0
  46. package/dist/packages/plugin-detail/src/DetailTabs.d.ts.map +1 -0
  47. package/dist/packages/plugin-detail/src/DetailView.d.ts +47 -0
  48. package/dist/packages/plugin-detail/src/DetailView.d.ts.map +1 -0
  49. package/dist/packages/plugin-detail/src/DetailView.stories.d.ts.map +1 -0
  50. package/dist/packages/plugin-detail/src/DiffView.d.ts.map +1 -0
  51. package/dist/packages/plugin-detail/src/FieldChangeItem.d.ts.map +1 -0
  52. package/dist/packages/plugin-detail/src/HeaderHighlight.d.ts.map +1 -0
  53. package/dist/packages/plugin-detail/src/InlineCreateRelated.d.ts.map +1 -0
  54. package/dist/packages/plugin-detail/src/MentionAutocomplete.d.ts.map +1 -0
  55. package/dist/packages/plugin-detail/src/PointInTimeRestore.d.ts.map +1 -0
  56. package/dist/packages/plugin-detail/src/ReactionPicker.d.ts.map +1 -0
  57. package/dist/packages/plugin-detail/src/RecordActivityTimeline.d.ts.map +1 -0
  58. package/dist/packages/plugin-detail/src/RecordChatterPanel.d.ts.map +1 -0
  59. package/dist/packages/plugin-detail/src/RecordComments.d.ts.map +1 -0
  60. package/dist/packages/plugin-detail/src/RecordNavigationEnhanced.d.ts.map +1 -0
  61. package/dist/{src → packages/plugin-detail/src}/RelatedList.d.ts +8 -0
  62. package/dist/packages/plugin-detail/src/RelatedList.d.ts.map +1 -0
  63. package/dist/packages/plugin-detail/src/RelationshipGraph.d.ts.map +1 -0
  64. package/dist/packages/plugin-detail/src/RichTextCommentInput.d.ts.map +1 -0
  65. package/dist/packages/plugin-detail/src/SectionGroup.d.ts.map +1 -0
  66. package/dist/packages/plugin-detail/src/SubscriptionToggle.d.ts.map +1 -0
  67. package/dist/packages/plugin-detail/src/ThreadedReplies.d.ts.map +1 -0
  68. package/dist/packages/plugin-detail/src/autoLayout.d.ts.map +1 -0
  69. package/dist/packages/plugin-detail/src/index.d.ts.map +1 -0
  70. package/dist/packages/plugin-detail/src/useDetailTranslation.d.ts.map +1 -0
  71. package/dist/plugin-detail.css +1 -2
  72. package/dist/rolldown-runtime-DnwLefa7.js +23 -0
  73. package/dist/{src-CXr1-vVl.js → src-DyUKLvMN.js} +29788 -37711
  74. package/dist/useFieldTranslation-BRgjC1oq.js +9 -0
  75. package/package.json +34 -12
  76. package/.turbo/turbo-build.log +0 -61
  77. package/dist/AddressField-DBkEyMcG.js +0 -93
  78. package/dist/AutoNumberField-Baa191z-.js +0 -14
  79. package/dist/CodeField-BU51nl1L.js +0 -22
  80. package/dist/ColorField-Cnf6ZM7c.js +0 -37
  81. package/dist/CurrencyField-Wg-XOId2.js +0 -51
  82. package/dist/DateField-Cth1ky_m.js +0 -21
  83. package/dist/DateTimeField-B0m6FhHL.js +0 -32
  84. package/dist/EmailField-Do7qT_L_.js +0 -28
  85. package/dist/FileField-aRJAdbQb.js +0 -151
  86. package/dist/FormulaField-DTMkagFx.js +0 -14
  87. package/dist/GeolocationField-RqpHWTEv.js +0 -113
  88. package/dist/GridField-D4IH0cpo.js +0 -51
  89. package/dist/ImageField-BYCFajjr.js +0 -75
  90. package/dist/LocationField-Bi_ew9sd.js +0 -35
  91. package/dist/LookupField-BjwlDPtt.js +0 -902
  92. package/dist/NumberField-D_NucQlp.js +0 -26
  93. package/dist/ObjectField-CG-LaM65.js +0 -52
  94. package/dist/PercentField-B6sO_J3i.js +0 -63
  95. package/dist/PhoneField-CcQAWwR6.js +0 -28
  96. package/dist/QRCodeField-CEjWs-J5.js +0 -72
  97. package/dist/RichTextField-qOEJl5Ai.js +0 -32
  98. package/dist/SelectField-C8hWu3gm.js +0 -30
  99. package/dist/SummaryField-DgiFm-Cr.js +0 -19
  100. package/dist/TextAreaField-DuriTqsD.js +0 -36
  101. package/dist/TextField-CGNSl7RU.js +0 -29
  102. package/dist/TimeField-YO58ctFg.js +0 -21
  103. package/dist/UrlField-1-BMM1jn.js +0 -33
  104. package/dist/UserField-B6GqxP_S.js +0 -78
  105. package/dist/VectorField-BkEjbSt0.js +0 -36
  106. package/dist/src/ActivityTimeline.d.ts.map +0 -1
  107. package/dist/src/CommentAttachment.d.ts.map +0 -1
  108. package/dist/src/CommentInput.d.ts.map +0 -1
  109. package/dist/src/DetailSection.d.ts.map +0 -1
  110. package/dist/src/DetailTabs.d.ts.map +0 -1
  111. package/dist/src/DetailView.d.ts +0 -23
  112. package/dist/src/DetailView.d.ts.map +0 -1
  113. package/dist/src/DetailView.stories.d.ts.map +0 -1
  114. package/dist/src/DiffView.d.ts.map +0 -1
  115. package/dist/src/FieldChangeItem.d.ts.map +0 -1
  116. package/dist/src/HeaderHighlight.d.ts.map +0 -1
  117. package/dist/src/InlineCreateRelated.d.ts.map +0 -1
  118. package/dist/src/MentionAutocomplete.d.ts.map +0 -1
  119. package/dist/src/PointInTimeRestore.d.ts.map +0 -1
  120. package/dist/src/ReactionPicker.d.ts.map +0 -1
  121. package/dist/src/RecordActivityTimeline.d.ts.map +0 -1
  122. package/dist/src/RecordChatterPanel.d.ts.map +0 -1
  123. package/dist/src/RecordComments.d.ts.map +0 -1
  124. package/dist/src/RecordNavigationEnhanced.d.ts.map +0 -1
  125. package/dist/src/RelatedList.d.ts.map +0 -1
  126. package/dist/src/RelationshipGraph.d.ts.map +0 -1
  127. package/dist/src/RichTextCommentInput.d.ts.map +0 -1
  128. package/dist/src/SectionGroup.d.ts.map +0 -1
  129. package/dist/src/SubscriptionToggle.d.ts.map +0 -1
  130. package/dist/src/ThreadedReplies.d.ts.map +0 -1
  131. package/dist/src/autoLayout.d.ts.map +0 -1
  132. package/dist/src/index.d.ts.map +0 -1
  133. package/dist/src/useDetailTranslation.d.ts.map +0 -1
  134. package/src/ActivityTimeline.tsx +0 -184
  135. package/src/CommentAttachment.tsx +0 -192
  136. package/src/CommentInput.tsx +0 -81
  137. package/src/DetailSection.tsx +0 -340
  138. package/src/DetailTabs.tsx +0 -73
  139. package/src/DetailView.stories.tsx +0 -334
  140. package/src/DetailView.tsx +0 -823
  141. package/src/DiffView.tsx +0 -231
  142. package/src/FieldChangeItem.tsx +0 -46
  143. package/src/HeaderHighlight.tsx +0 -88
  144. package/src/InlineCreateRelated.tsx +0 -291
  145. package/src/MentionAutocomplete.tsx +0 -123
  146. package/src/PointInTimeRestore.tsx +0 -261
  147. package/src/ReactionPicker.tsx +0 -106
  148. package/src/RecordActivityTimeline.tsx +0 -429
  149. package/src/RecordChatterPanel.tsx +0 -207
  150. package/src/RecordComments.tsx +0 -215
  151. package/src/RecordNavigationEnhanced.tsx +0 -211
  152. package/src/RelatedList.tsx +0 -413
  153. package/src/RelationshipGraph.tsx +0 -286
  154. package/src/RichTextCommentInput.tsx +0 -348
  155. package/src/SectionGroup.tsx +0 -101
  156. package/src/SubscriptionToggle.tsx +0 -60
  157. package/src/ThreadedReplies.tsx +0 -161
  158. package/src/__tests__/ActivityTimeline.test.tsx +0 -119
  159. package/src/__tests__/ActivityTimelineFiltering.test.tsx +0 -143
  160. package/src/__tests__/CommentInput.test.tsx +0 -57
  161. package/src/__tests__/DetailSection.test.tsx +0 -490
  162. package/src/__tests__/DetailView.test.tsx +0 -694
  163. package/src/__tests__/FieldChangeItem.test.tsx +0 -119
  164. package/src/__tests__/HeaderHighlight.test.tsx +0 -213
  165. package/src/__tests__/MentionAutocomplete.test.tsx +0 -97
  166. package/src/__tests__/ReactionPicker.test.tsx +0 -113
  167. package/src/__tests__/RecordActivityTimeline.test.tsx +0 -395
  168. package/src/__tests__/RecordChatterPanel.test.tsx +0 -265
  169. package/src/__tests__/RecordComments.test.tsx +0 -96
  170. package/src/__tests__/RecordCommentsPinSearch.test.tsx +0 -133
  171. package/src/__tests__/RelatedList.test.tsx +0 -160
  172. package/src/__tests__/SectionGroup.test.tsx +0 -101
  173. package/src/__tests__/SubscriptionToggle.test.tsx +0 -84
  174. package/src/__tests__/ThreadedReplies.test.tsx +0 -212
  175. package/src/__tests__/autoLayout.test.ts +0 -228
  176. package/src/__tests__/phase12-features.test.tsx +0 -583
  177. package/src/__tests__/roadmap-features.test.tsx +0 -478
  178. package/src/autoLayout.ts +0 -128
  179. package/src/index.tsx +0 -149
  180. package/src/useDetailTranslation.ts +0 -114
  181. package/tsconfig.json +0 -18
  182. package/vite.config.ts +0 -56
  183. package/vitest.config.ts +0 -13
  184. package/vitest.setup.ts +0 -1
  185. /package/dist/{src → packages/plugin-detail/src}/ActivityTimeline.d.ts +0 -0
  186. /package/dist/{src → packages/plugin-detail/src}/CommentAttachment.d.ts +0 -0
  187. /package/dist/{src → packages/plugin-detail/src}/CommentInput.d.ts +0 -0
  188. /package/dist/{src → packages/plugin-detail/src}/DetailSection.d.ts +0 -0
  189. /package/dist/{src → packages/plugin-detail/src}/DetailTabs.d.ts +0 -0
  190. /package/dist/{src → packages/plugin-detail/src}/DetailView.stories.d.ts +0 -0
  191. /package/dist/{src → packages/plugin-detail/src}/DiffView.d.ts +0 -0
  192. /package/dist/{src → packages/plugin-detail/src}/FieldChangeItem.d.ts +0 -0
  193. /package/dist/{src → packages/plugin-detail/src}/HeaderHighlight.d.ts +0 -0
  194. /package/dist/{src → packages/plugin-detail/src}/InlineCreateRelated.d.ts +0 -0
  195. /package/dist/{src → packages/plugin-detail/src}/MentionAutocomplete.d.ts +0 -0
  196. /package/dist/{src → packages/plugin-detail/src}/PointInTimeRestore.d.ts +0 -0
  197. /package/dist/{src → packages/plugin-detail/src}/ReactionPicker.d.ts +0 -0
  198. /package/dist/{src → packages/plugin-detail/src}/RecordActivityTimeline.d.ts +0 -0
  199. /package/dist/{src → packages/plugin-detail/src}/RecordChatterPanel.d.ts +0 -0
  200. /package/dist/{src → packages/plugin-detail/src}/RecordComments.d.ts +0 -0
  201. /package/dist/{src → packages/plugin-detail/src}/RecordNavigationEnhanced.d.ts +0 -0
  202. /package/dist/{src → packages/plugin-detail/src}/RelationshipGraph.d.ts +0 -0
  203. /package/dist/{src → packages/plugin-detail/src}/RichTextCommentInput.d.ts +0 -0
  204. /package/dist/{src → packages/plugin-detail/src}/SectionGroup.d.ts +0 -0
  205. /package/dist/{src → packages/plugin-detail/src}/SubscriptionToggle.d.ts +0 -0
  206. /package/dist/{src → packages/plugin-detail/src}/ThreadedReplies.d.ts +0 -0
  207. /package/dist/{src → packages/plugin-detail/src}/autoLayout.d.ts +0 -0
  208. /package/dist/{src → packages/plugin-detail/src}/index.d.ts +0 -0
  209. /package/dist/{src → packages/plugin-detail/src}/useDetailTranslation.d.ts +0 -0
@@ -1,340 +0,0 @@
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
- import * as React from 'react';
10
- import {
11
- cn,
12
- Card,
13
- CardHeader,
14
- CardTitle,
15
- CardContent,
16
- Collapsible,
17
- CollapsibleTrigger,
18
- CollapsibleContent,
19
- Badge,
20
- Button,
21
- Tooltip,
22
- TooltipContent,
23
- TooltipProvider,
24
- TooltipTrigger,
25
- } from '@object-ui/components';
26
- import { ChevronDown, ChevronRight, Copy, Check } from 'lucide-react';
27
- import { SchemaRenderer } from '@object-ui/react';
28
- import { getCellRenderer } from '@object-ui/fields';
29
- import type { DetailViewSection as DetailViewSectionType, DetailViewField, FieldMetadata } from '@object-ui/types';
30
- import { applyDetailAutoLayout } from './autoLayout';
31
- import { useDetailTranslation } from './useDetailTranslation';
32
- import { useSafeFieldLabel } from '@object-ui/react';
33
-
34
- /**
35
- * Compute responsive col-span classes so that col-span never exceeds the
36
- * visible column count at each Tailwind breakpoint.
37
- *
38
- * For columns=1: no span class (always single column)
39
- * For columns=2: md:col-span-{min(span,2)}
40
- * For columns>=3: md:col-span-{min(span,2)} lg:col-span-{min(span,3)}
41
- */
42
- export function getResponsiveSpanClass(span: number | undefined, columns: number): string {
43
- if (!span || span <= 1 || columns <= 1) return '';
44
-
45
- if (columns === 2) {
46
- return span >= 2 ? 'md:col-span-2' : '';
47
- }
48
-
49
- // columns >= 3: grid-cols-1 md:grid-cols-2 lg:grid-cols-3
50
- if (span === 2) return 'md:col-span-2';
51
- if (span >= 3) return 'md:col-span-2 lg:col-span-3';
52
-
53
- return '';
54
- }
55
-
56
- export interface VirtualScrollOptions {
57
- /** Enable virtual scrolling for large field sets */
58
- enabled?: boolean;
59
- /** Height of each field row in px (default: 60) */
60
- itemHeight?: number;
61
- /** Number of fields to render in the initial batch before revealing all (default: 20) */
62
- batchSize?: number;
63
- }
64
-
65
- export interface DetailSectionProps {
66
- section: DetailViewSectionType;
67
- data?: any;
68
- className?: string;
69
- /** Object schema from DataSource for field type enrichment */
70
- objectSchema?: any;
71
- /** Object name for i18n field label resolution */
72
- objectName?: string;
73
- /** Whether inline editing is active */
74
- isEditing?: boolean;
75
- /** Callback when a field value changes during inline editing */
76
- onFieldChange?: (field: string, value: any) => void;
77
- /** Virtual scrolling configuration for sections with many fields */
78
- virtualScroll?: VirtualScrollOptions;
79
- }
80
-
81
- export const DetailSection: React.FC<DetailSectionProps> = ({
82
- section,
83
- data,
84
- className,
85
- objectSchema,
86
- objectName,
87
- isEditing = false,
88
- onFieldChange,
89
- virtualScroll,
90
- }) => {
91
- const [isCollapsed, setIsCollapsed] = React.useState(section.defaultCollapsed ?? false);
92
- const [copiedField, setCopiedField] = React.useState<string | null>(null);
93
- const [visibleCount, setVisibleCount] = React.useState<number | undefined>(undefined);
94
- const { t } = useDetailTranslation();
95
- const { fieldLabel } = useSafeFieldLabel();
96
-
97
- const handleCopyField = React.useCallback((fieldName: string, value: any) => {
98
- const textValue = value !== null && value !== undefined ? String(value) : '';
99
- navigator.clipboard.writeText(textValue).then(() => {
100
- setCopiedField(fieldName);
101
- setTimeout(() => setCopiedField(null), 2000);
102
- });
103
- }, []);
104
-
105
- // Filter out empty fields when hideEmpty is set
106
- const visibleFields = section.hideEmpty
107
- ? section.fields.filter((field) => {
108
- const value = data?.[field.name] ?? field.value;
109
- return value !== null && value !== undefined && value !== '';
110
- })
111
- : section.fields;
112
-
113
- // Hide entire section when all fields are empty
114
- if (visibleFields.length === 0) return null;
115
-
116
- // Apply auto-layout: infer columns and auto-span wide fields
117
- const { fields: layoutFields, columns: effectiveColumns } = applyDetailAutoLayout(
118
- visibleFields,
119
- section.columns
120
- );
121
-
122
- const renderField = (field: DetailViewField) => {
123
- const value = data?.[field.name] ?? field.value;
124
-
125
- // If custom renderer provided
126
- if (field.render) {
127
- return <SchemaRenderer schema={field.render} data={{ ...data, value }} />;
128
- }
129
-
130
- // Calculate responsive span class so col-span never exceeds the visible
131
- // column count at each breakpoint, preventing implicit columns on mobile.
132
- const spanClass = getResponsiveSpanClass(field.span, effectiveColumns);
133
-
134
- const displayValue = (() => {
135
- if (value === null || value === undefined) return <span className="text-muted-foreground/50 text-xs italic">—</span>;
136
- // Enrich field with objectSchema metadata — merge missing properties
137
- // even when field.type is explicitly set (e.g., type: 'lookup' without reference_to)
138
- const objectDefField = objectSchema?.fields?.[field.name];
139
- const resolvedType = field.type || objectDefField?.type;
140
- const enrichedField: Record<string, any> = { ...field };
141
- if (objectDefField) {
142
- if (!field.type && objectDefField.type) enrichedField.type = objectDefField.type;
143
- if (objectDefField.options && !enrichedField.options) enrichedField.options = objectDefField.options;
144
- if (objectDefField.currency && !enrichedField.currency) enrichedField.currency = objectDefField.currency;
145
- if (objectDefField.precision !== undefined && enrichedField.precision === undefined) enrichedField.precision = objectDefField.precision;
146
- if (objectDefField.format && !enrichedField.format) enrichedField.format = objectDefField.format;
147
- const refTarget = objectDefField.reference_to || objectDefField.reference;
148
- if (refTarget && !enrichedField.reference_to) enrichedField.reference_to = refTarget;
149
- if (objectDefField.reference_field && !enrichedField.reference_field) enrichedField.reference_field = objectDefField.reference_field;
150
- }
151
- // Use type-aware cell renderer when field type is available (explicit or enriched)
152
- if (resolvedType) {
153
- const CellRenderer = getCellRenderer(resolvedType);
154
- if (CellRenderer) {
155
- return <CellRenderer value={value} field={enrichedField as unknown as FieldMetadata} />;
156
- }
157
- }
158
- return String(value);
159
- })();
160
- const canCopy = value !== null && value !== undefined && value !== '';
161
- const isCopied = copiedField === field.name;
162
-
163
- // Default field rendering with copy button and touch-friendly targets
164
- return (
165
- <div key={field.name} className={cn("space-y-1.5 group", spanClass)}>
166
- <div className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
167
- {fieldLabel(objectName || '', field.name, field.label || field.name)}
168
- </div>
169
- {isEditing && !field.readonly ? (
170
- <div className="min-h-[44px] sm:min-h-0">
171
- <input
172
- type={field.type === 'number' ? 'number' : field.type === 'date' ? 'date' : 'text'}
173
- className="w-full px-2 py-1.5 text-sm border rounded-md bg-background focus:outline-none focus:ring-2 focus:ring-ring"
174
- value={value != null ? String(value) : ''}
175
- onChange={(e) => onFieldChange?.(field.name, e.target.value)}
176
- />
177
- </div>
178
- ) : (
179
- <div
180
- className={cn(
181
- "flex items-start justify-between gap-2 min-h-[44px] sm:min-h-0 rounded-md",
182
- canCopy && "cursor-pointer active:bg-muted/60 transition-colors"
183
- )}
184
- onClick={canCopy ? () => handleCopyField(field.name, value) : undefined}
185
- onKeyDown={canCopy ? (e: React.KeyboardEvent) => {
186
- if (e.key === 'Enter' || e.key === ' ') {
187
- e.preventDefault();
188
- handleCopyField(field.name, value);
189
- }
190
- } : undefined}
191
- role={canCopy ? "button" : undefined}
192
- tabIndex={canCopy ? 0 : undefined}
193
- >
194
- <div className="text-sm flex-1 break-words py-1">
195
- {displayValue}
196
- </div>
197
- {canCopy && (
198
- <TooltipProvider>
199
- <Tooltip>
200
- <TooltipTrigger asChild>
201
- <Button
202
- variant="ghost"
203
- size="icon"
204
- className="h-6 w-6 opacity-0 group-hover:opacity-100 transition-opacity shrink-0"
205
- onClick={(e) => {
206
- e.stopPropagation();
207
- handleCopyField(field.name, value);
208
- }}
209
- >
210
- {isCopied ? (
211
- <Check className="h-3 w-3 text-green-600" />
212
- ) : (
213
- <Copy className="h-3 w-3" />
214
- )}
215
- </Button>
216
- </TooltipTrigger>
217
- <TooltipContent>
218
- {isCopied ? t('detail.copied') : t('detail.copyToClipboard')}
219
- </TooltipContent>
220
- </Tooltip>
221
- </TooltipProvider>
222
- )}
223
- </div>
224
- )}
225
- </div>
226
- );
227
- };
228
-
229
- // Virtual scroll: progressive batch rendering for large field sets
230
- const vsEnabled = virtualScroll?.enabled === true;
231
- const vsBatchSize = virtualScroll?.batchSize ?? 20;
232
- /** Delay (ms) before revealing remaining fields after the initial batch */
233
- const VS_REVEAL_DELAY = 100;
234
-
235
- React.useEffect(() => {
236
- if (!vsEnabled) {
237
- setVisibleCount(undefined);
238
- return;
239
- }
240
- // Start with a batch, then progressively reveal more
241
- if (layoutFields.length <= vsBatchSize) {
242
- setVisibleCount(undefined);
243
- return;
244
- }
245
- setVisibleCount(vsBatchSize);
246
- const timer = setTimeout(() => setVisibleCount(undefined), VS_REVEAL_DELAY);
247
- return () => clearTimeout(timer);
248
- // eslint-disable-next-line react-hooks/exhaustive-deps
249
- }, [vsEnabled, layoutFields.length, vsBatchSize]);
250
-
251
- const renderedFields = visibleCount !== undefined
252
- ? layoutFields.slice(0, visibleCount)
253
- : layoutFields;
254
-
255
- const content = (
256
- <div
257
- className={cn(
258
- "grid gap-3 sm:gap-4",
259
- effectiveColumns === 1 ? "grid-cols-1" :
260
- effectiveColumns === 2 ? "grid-cols-1 md:grid-cols-2" :
261
- effectiveColumns === 3 ? "grid-cols-1 md:grid-cols-2 lg:grid-cols-3" :
262
- "grid-cols-1 md:grid-cols-2 lg:grid-cols-3"
263
- )}
264
- >
265
- {renderedFields.map(renderField)}
266
- </div>
267
- );
268
-
269
- if (!section.collapsible) {
270
- return (
271
- <Card className={cn(section.showBorder === false ? 'border-none shadow-none' : '', className)}>
272
- {section.title && (
273
- <CardHeader className={cn(section.headerColor && `bg-${section.headerColor}`)}>
274
- <CardTitle className="flex items-center justify-between">
275
- <div className="flex items-center gap-2">
276
- {section.icon && <span className="text-muted-foreground">{section.icon}</span>}
277
- <span>{section.title}</span>
278
- {section.fields && (
279
- <Badge variant="secondary" className="ml-2 text-xs">
280
- {section.fields.length}
281
- </Badge>
282
- )}
283
- </div>
284
- </CardTitle>
285
- {section.description && (
286
- <p className="text-sm text-muted-foreground mt-1.5">{section.description}</p>
287
- )}
288
- </CardHeader>
289
- )}
290
- <CardContent className="pt-4 sm:pt-6 px-3 sm:px-6">
291
- {content}
292
- </CardContent>
293
- </Card>
294
- );
295
- }
296
-
297
- return (
298
- <Collapsible
299
- open={!isCollapsed}
300
- onOpenChange={(open) => setIsCollapsed(!open)}
301
- className={className}
302
- >
303
- <Card>
304
- <CollapsibleTrigger asChild>
305
- <CardHeader className={cn(
306
- "cursor-pointer hover:bg-muted/50 transition-colors",
307
- section.headerColor && `bg-${section.headerColor}`
308
- )}>
309
- <CardTitle className="flex items-center justify-between">
310
- <div className="flex items-center gap-2">
311
- {section.icon && <span className="text-muted-foreground">{section.icon}</span>}
312
- <span>{section.title}</span>
313
- {section.fields && (
314
- <Badge variant="secondary" className="ml-2 text-xs">
315
- {section.fields.length}
316
- </Badge>
317
- )}
318
- </div>
319
- <div className="flex items-center gap-2">
320
- {isCollapsed ? (
321
- <ChevronRight className="h-4 w-4 text-muted-foreground" />
322
- ) : (
323
- <ChevronDown className="h-4 w-4 text-muted-foreground" />
324
- )}
325
- </div>
326
- </CardTitle>
327
- {section.description && !isCollapsed && (
328
- <p className="text-sm text-muted-foreground mt-1.5">{section.description}</p>
329
- )}
330
- </CardHeader>
331
- </CollapsibleTrigger>
332
- <CollapsibleContent>
333
- <CardContent className="pt-4 sm:pt-6 px-3 sm:px-6">
334
- {content}
335
- </CardContent>
336
- </CollapsibleContent>
337
- </Card>
338
- </Collapsible>
339
- );
340
- };
@@ -1,73 +0,0 @@
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
- import * as React from 'react';
10
- import { Tabs, TabsList, TabsTrigger, TabsContent, Badge } from '@object-ui/components';
11
- import { SchemaRenderer } from '@object-ui/react';
12
- import type { DetailViewTab } from '@object-ui/types';
13
-
14
- export interface DetailTabsProps {
15
- tabs: DetailViewTab[];
16
- data?: any;
17
- className?: string;
18
- }
19
-
20
- export const DetailTabs: React.FC<DetailTabsProps> = ({
21
- tabs,
22
- data,
23
- className,
24
- }) => {
25
- const [activeTab, setActiveTab] = React.useState(tabs[0]?.key);
26
-
27
- const visibleTabs = tabs.filter(tab => {
28
- if (typeof tab.visible === 'boolean') return tab.visible;
29
- if (typeof tab.visible === 'string') {
30
- // Simple expression evaluation could go here
31
- return true;
32
- }
33
- return true;
34
- });
35
-
36
- return (
37
- <Tabs value={activeTab} onValueChange={setActiveTab} className={className}>
38
- <TabsList className="w-full justify-start border-b rounded-none bg-transparent p-0">
39
- {visibleTabs.map((tab) => (
40
- <TabsTrigger
41
- key={tab.key}
42
- value={tab.key}
43
- className="relative rounded-none border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:bg-transparent"
44
- >
45
- <div className="flex items-center gap-2">
46
- {tab.icon && <span>{tab.icon}</span>}
47
- <span>{tab.label}</span>
48
- {tab.badge && (
49
- <Badge variant="secondary" className="ml-1">
50
- {tab.badge}
51
- </Badge>
52
- )}
53
- </div>
54
- </TabsTrigger>
55
- ))}
56
- </TabsList>
57
-
58
- {visibleTabs.map((tab) => (
59
- <TabsContent key={tab.key} value={tab.key} className="mt-4">
60
- {Array.isArray(tab.content) ? (
61
- <div className="space-y-4">
62
- {tab.content.map((schema, index) => (
63
- <SchemaRenderer key={index} schema={schema} data={data} />
64
- ))}
65
- </div>
66
- ) : (
67
- <SchemaRenderer schema={tab.content} data={data} />
68
- )}
69
- </TabsContent>
70
- ))}
71
- </Tabs>
72
- );
73
- };