@object-ui/plugin-detail 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@object-ui/plugin-detail",
3
- "version": "0.5.0",
3
+ "version": "3.0.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "DetailView plugin for Object UI - comprehensive detail page with sections, tabs, and related lists",
@@ -25,19 +25,19 @@
25
25
  },
26
26
  "dependencies": {
27
27
  "lucide-react": "^0.563.0",
28
- "@object-ui/components": "0.5.0",
29
- "@object-ui/core": "0.5.0",
30
- "@object-ui/react": "0.5.0",
31
- "@object-ui/types": "0.5.0"
28
+ "@object-ui/components": "3.0.0",
29
+ "@object-ui/core": "3.0.0",
30
+ "@object-ui/react": "3.0.0",
31
+ "@object-ui/types": "3.0.0"
32
32
  },
33
33
  "peerDependencies": {
34
34
  "react": "^18.0.0 || ^19.0.0",
35
35
  "react-dom": "^18.0.0 || ^19.0.0"
36
36
  },
37
37
  "devDependencies": {
38
- "@types/react": "^19.2.10",
39
- "@types/react-dom": "^19.2.3",
40
- "@vitejs/plugin-react": "^5.1.3",
38
+ "@types/react": "19.2.13",
39
+ "@types/react-dom": "19.2.3",
40
+ "@vitejs/plugin-react": "^5.1.4",
41
41
  "typescript": "^5.9.3",
42
42
  "vite": "^7.3.1",
43
43
  "vite-plugin-dts": "^4.5.4",
@@ -7,8 +7,23 @@
7
7
  */
8
8
 
9
9
  import * as React from 'react';
10
- import { cn, Card, CardHeader, CardTitle, CardContent, Collapsible, CollapsibleTrigger, CollapsibleContent } from '@object-ui/components';
11
- import { ChevronDown, ChevronRight } from 'lucide-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';
12
27
  import { SchemaRenderer } from '@object-ui/react';
13
28
  import type { DetailViewSection as DetailViewSectionType } from '@object-ui/types';
14
29
 
@@ -24,6 +39,15 @@ export const DetailSection: React.FC<DetailSectionProps> = ({
24
39
  className,
25
40
  }) => {
26
41
  const [isCollapsed, setIsCollapsed] = React.useState(section.defaultCollapsed ?? false);
42
+ const [copiedField, setCopiedField] = React.useState<string | null>(null);
43
+
44
+ const handleCopyField = React.useCallback((fieldName: string, value: any) => {
45
+ const textValue = value !== null && value !== undefined ? String(value) : '';
46
+ navigator.clipboard.writeText(textValue).then(() => {
47
+ setCopiedField(fieldName);
48
+ setTimeout(() => setCopiedField(null), 2000);
49
+ });
50
+ }, []);
27
51
 
28
52
  const renderField = (field: any) => {
29
53
  const value = data?.[field.name] ?? field.value;
@@ -41,14 +65,60 @@ export const DetailSection: React.FC<DetailSectionProps> = ({
41
65
  field.span === 5 ? 'col-span-5' :
42
66
  field.span === 6 ? 'col-span-6' : '';
43
67
 
44
- // Default field rendering
68
+ const displayValue = value !== null && value !== undefined ? String(value) : '-';
69
+ const canCopy = value !== null && value !== undefined && value !== '';
70
+ const isCopied = copiedField === field.name;
71
+
72
+ // Default field rendering with copy button and touch-friendly targets
45
73
  return (
46
- <div key={field.name} className={cn("space-y-1", spanClass)}>
47
- <div className="text-sm font-medium text-muted-foreground">
74
+ <div key={field.name} className={cn("space-y-1.5 group", spanClass)}>
75
+ <div className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
48
76
  {field.label || field.name}
49
77
  </div>
50
- <div className="text-sm">
51
- {value !== null && value !== undefined ? String(value) : '-'}
78
+ <div
79
+ className={cn(
80
+ "flex items-start justify-between gap-2 min-h-[44px] sm:min-h-0 rounded-md",
81
+ canCopy && "cursor-pointer active:bg-muted/60 transition-colors"
82
+ )}
83
+ onClick={canCopy ? () => handleCopyField(field.name, value) : undefined}
84
+ onKeyDown={canCopy ? (e: React.KeyboardEvent) => {
85
+ if (e.key === 'Enter' || e.key === ' ') {
86
+ e.preventDefault();
87
+ handleCopyField(field.name, value);
88
+ }
89
+ } : undefined}
90
+ role={canCopy ? "button" : undefined}
91
+ tabIndex={canCopy ? 0 : undefined}
92
+ >
93
+ <div className="text-sm flex-1 break-words py-1">
94
+ {displayValue}
95
+ </div>
96
+ {canCopy && (
97
+ <TooltipProvider>
98
+ <Tooltip>
99
+ <TooltipTrigger asChild>
100
+ <Button
101
+ variant="ghost"
102
+ size="icon"
103
+ className="h-6 w-6 opacity-0 group-hover:opacity-100 transition-opacity shrink-0"
104
+ onClick={(e) => {
105
+ e.stopPropagation();
106
+ handleCopyField(field.name, value);
107
+ }}
108
+ >
109
+ {isCopied ? (
110
+ <Check className="h-3 w-3 text-green-600" />
111
+ ) : (
112
+ <Copy className="h-3 w-3" />
113
+ )}
114
+ </Button>
115
+ </TooltipTrigger>
116
+ <TooltipContent>
117
+ {isCopied ? 'Copied!' : 'Copy to clipboard'}
118
+ </TooltipContent>
119
+ </Tooltip>
120
+ </TooltipProvider>
121
+ )}
52
122
  </div>
53
123
  </div>
54
124
  );
@@ -57,11 +127,11 @@ export const DetailSection: React.FC<DetailSectionProps> = ({
57
127
  const content = (
58
128
  <div
59
129
  className={cn(
60
- "grid gap-4",
130
+ "grid gap-3 sm:gap-4",
61
131
  section.columns === 1 ? "grid-cols-1" :
62
- section.columns === 2 ? "grid-cols-2" :
63
- section.columns === 3 ? "grid-cols-3" :
64
- "grid-cols-2 md:grid-cols-3"
132
+ section.columns === 2 ? "grid-cols-1 sm:grid-cols-2" :
133
+ section.columns === 3 ? "grid-cols-1 sm:grid-cols-2 md:grid-cols-3" :
134
+ "grid-cols-1 sm:grid-cols-2 md:grid-cols-3"
65
135
  )}
66
136
  >
67
137
  {section.fields.map(renderField)}
@@ -70,19 +140,26 @@ export const DetailSection: React.FC<DetailSectionProps> = ({
70
140
 
71
141
  if (!section.collapsible) {
72
142
  return (
73
- <Card className={className}>
143
+ <Card className={cn(section.showBorder === false ? 'border-none shadow-none' : '', className)}>
74
144
  {section.title && (
75
- <CardHeader>
76
- <CardTitle className="flex items-center gap-2">
77
- {section.icon && <span className="text-muted-foreground">{section.icon}</span>}
78
- <span>{section.title}</span>
145
+ <CardHeader className={cn(section.headerColor && `bg-${section.headerColor}`)}>
146
+ <CardTitle className="flex items-center justify-between">
147
+ <div className="flex items-center gap-2">
148
+ {section.icon && <span className="text-muted-foreground">{section.icon}</span>}
149
+ <span>{section.title}</span>
150
+ {section.fields && (
151
+ <Badge variant="secondary" className="ml-2 text-xs">
152
+ {section.fields.length}
153
+ </Badge>
154
+ )}
155
+ </div>
79
156
  </CardTitle>
80
157
  {section.description && (
81
- <p className="text-sm text-muted-foreground mt-1">{section.description}</p>
158
+ <p className="text-sm text-muted-foreground mt-1.5">{section.description}</p>
82
159
  )}
83
160
  </CardHeader>
84
161
  )}
85
- <CardContent>
162
+ <CardContent className="pt-4 sm:pt-6 px-3 sm:px-6">
86
163
  {content}
87
164
  </CardContent>
88
165
  </Card>
@@ -97,21 +174,35 @@ export const DetailSection: React.FC<DetailSectionProps> = ({
97
174
  >
98
175
  <Card>
99
176
  <CollapsibleTrigger asChild>
100
- <CardHeader className="cursor-pointer hover:bg-muted/50 transition-colors">
177
+ <CardHeader className={cn(
178
+ "cursor-pointer hover:bg-muted/50 transition-colors",
179
+ section.headerColor && `bg-${section.headerColor}`
180
+ )}>
101
181
  <CardTitle className="flex items-center justify-between">
102
182
  <div className="flex items-center gap-2">
103
183
  {section.icon && <span className="text-muted-foreground">{section.icon}</span>}
104
184
  <span>{section.title}</span>
185
+ {section.fields && (
186
+ <Badge variant="secondary" className="ml-2 text-xs">
187
+ {section.fields.length}
188
+ </Badge>
189
+ )}
190
+ </div>
191
+ <div className="flex items-center gap-2">
192
+ {isCollapsed ? (
193
+ <ChevronRight className="h-4 w-4 text-muted-foreground" />
194
+ ) : (
195
+ <ChevronDown className="h-4 w-4 text-muted-foreground" />
196
+ )}
105
197
  </div>
106
- {isCollapsed ? <ChevronRight className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
107
198
  </CardTitle>
108
199
  {section.description && !isCollapsed && (
109
- <p className="text-sm text-muted-foreground mt-1">{section.description}</p>
200
+ <p className="text-sm text-muted-foreground mt-1.5">{section.description}</p>
110
201
  )}
111
202
  </CardHeader>
112
203
  </CollapsibleTrigger>
113
204
  <CollapsibleContent>
114
- <CardContent>
205
+ <CardContent className="pt-4 sm:pt-6 px-3 sm:px-6">
115
206
  {content}
116
207
  </CardContent>
117
208
  </CollapsibleContent>
@@ -0,0 +1,258 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { SchemaRenderer } from '@object-ui/react';
3
+ import type { BaseSchema } from '@object-ui/types';
4
+ import { Building2, User } from 'lucide-react';
5
+
6
+ const meta = {
7
+ title: 'Plugins/DetailView',
8
+ component: SchemaRenderer,
9
+ parameters: {
10
+ layout: 'padded',
11
+ docs: {
12
+ description: {
13
+ component: 'Enhanced detail view component with Airtable-inspired features: tooltips, functional menus, collapsible sections, and copy-to-clipboard.',
14
+ },
15
+ },
16
+ },
17
+ tags: ['autodocs'],
18
+ argTypes: {
19
+ schema: { table: { disable: true } },
20
+ },
21
+ } satisfies Meta<any>;
22
+
23
+ export default meta;
24
+ type Story = StoryObj<typeof meta>;
25
+
26
+ const renderStory = (args: any) => <SchemaRenderer schema={args as unknown as BaseSchema} />;
27
+
28
+ export const Default: Story = {
29
+ render: renderStory,
30
+ args: {
31
+ type: 'detail-view',
32
+ title: 'Employee Details',
33
+ data: {
34
+ name: 'Sarah Connor',
35
+ email: 'sarah.connor@example.com',
36
+ phone: '+1 (555) 867-5309',
37
+ department: 'Engineering',
38
+ role: 'Tech Lead',
39
+ location: 'Austin, TX',
40
+ },
41
+ fields: [
42
+ { name: 'name', label: 'Full Name' },
43
+ { name: 'email', label: 'Email' },
44
+ { name: 'phone', label: 'Phone' },
45
+ { name: 'department', label: 'Department' },
46
+ { name: 'role', label: 'Role' },
47
+ { name: 'location', label: 'Location' },
48
+ ],
49
+ showBack: true,
50
+ showEdit: true,
51
+ showDelete: true,
52
+ } as any,
53
+ };
54
+
55
+ export const WithSections: Story = {
56
+ render: renderStory,
57
+ args: {
58
+ type: 'detail-view',
59
+ title: 'Account: TechCorp Inc.',
60
+ data: {
61
+ name: 'TechCorp Inc.',
62
+ industry: 'Software',
63
+ website: 'https://techcorp.io',
64
+ employees: '200-500',
65
+ revenue: '$25M - $50M',
66
+ street: '456 Innovation Blvd',
67
+ city: 'Seattle',
68
+ state: 'WA',
69
+ zipcode: '98101',
70
+ country: 'USA',
71
+ },
72
+ sections: [
73
+ {
74
+ title: 'Company Info',
75
+ icon: '🏢',
76
+ fields: [
77
+ { name: 'name', label: 'Company Name' },
78
+ { name: 'industry', label: 'Industry' },
79
+ { name: 'website', label: 'Website' },
80
+ { name: 'employees', label: 'Employees' },
81
+ { name: 'revenue', label: 'Annual Revenue' },
82
+ ],
83
+ columns: 2,
84
+ },
85
+ {
86
+ title: 'Address',
87
+ icon: '📍',
88
+ collapsible: true,
89
+ fields: [
90
+ { name: 'street', label: 'Street' },
91
+ { name: 'city', label: 'City' },
92
+ { name: 'state', label: 'State' },
93
+ { name: 'zipcode', label: 'Zip Code' },
94
+ { name: 'country', label: 'Country' },
95
+ ],
96
+ columns: 2,
97
+ },
98
+ ],
99
+ showBack: true,
100
+ showEdit: true,
101
+ showDelete: true,
102
+ } as any,
103
+ };
104
+
105
+ export const WithRelatedLists: Story = {
106
+ render: renderStory,
107
+ args: {
108
+ type: 'detail-view',
109
+ title: 'Account: TechCorp Inc.',
110
+ data: {
111
+ name: 'TechCorp Inc.',
112
+ industry: 'Software',
113
+ },
114
+ fields: [
115
+ { name: 'name', label: 'Account Name' },
116
+ { name: 'industry', label: 'Industry' },
117
+ ],
118
+ related: [
119
+ {
120
+ title: 'Contacts',
121
+ type: 'table',
122
+ data: [
123
+ { id: '1', name: 'Mike Ross', email: 'mike@techcorp.io', title: 'VP Engineering' },
124
+ { id: '2', name: 'Lisa Chen', email: 'lisa@techcorp.io', title: 'Product Manager' },
125
+ ],
126
+ columns: ['name', 'email', 'title'],
127
+ },
128
+ {
129
+ title: 'Opportunities',
130
+ type: 'table',
131
+ data: [
132
+ { id: '1', name: 'Enterprise License', amount: '$120,000', stage: 'Negotiation' },
133
+ { id: '2', name: 'Support Contract', amount: '$45,000', stage: 'Proposal' },
134
+ ],
135
+ columns: ['name', 'amount', 'stage'],
136
+ },
137
+ ],
138
+ showBack: true,
139
+ showEdit: true,
140
+ showDelete: true,
141
+ } as any,
142
+ };
143
+
144
+ /**
145
+ * Enhanced view showcasing new Airtable-inspired features
146
+ */
147
+ export const EnhancedAirtableStyle: Story = {
148
+ render: renderStory,
149
+ args: {
150
+ type: 'detail-view',
151
+ title: 'Sarah Johnson',
152
+ objectName: 'Contact',
153
+ resourceId: 'CNT-001',
154
+ showEdit: true,
155
+ showDelete: true,
156
+ showBack: true,
157
+ data: {
158
+ name: 'Sarah Johnson',
159
+ email: 'sarah.johnson@example.com',
160
+ phone: '+1 (555) 123-4567',
161
+ company: 'Acme Corporation',
162
+ title: 'Senior Product Manager',
163
+ department: 'Product',
164
+ location: 'San Francisco, CA',
165
+ website: 'https://example.com',
166
+ notes: 'Key decision maker for enterprise deals. Prefers email communication.',
167
+ linkedin: 'linkedin.com/in/sarahjohnson',
168
+ twitter: '@sarahjohnson',
169
+ status: 'Active',
170
+ lead_source: 'Website',
171
+ },
172
+ sections: [
173
+ {
174
+ title: 'Contact Information',
175
+ description: 'Primary contact details',
176
+ collapsible: false,
177
+ columns: 2,
178
+ fields: [
179
+ { name: 'email', label: 'Email' },
180
+ { name: 'phone', label: 'Phone' },
181
+ { name: 'title', label: 'Job Title' },
182
+ { name: 'department', label: 'Department' },
183
+ { name: 'location', label: 'Location' },
184
+ { name: 'website', label: 'Website' },
185
+ ],
186
+ },
187
+ {
188
+ title: 'Company Details',
189
+ collapsible: true,
190
+ defaultCollapsed: false,
191
+ columns: 2,
192
+ fields: [
193
+ { name: 'company', label: 'Company Name' },
194
+ { name: 'status', label: 'Status' },
195
+ { name: 'lead_source', label: 'Lead Source' },
196
+ ],
197
+ },
198
+ {
199
+ title: 'Social Media',
200
+ collapsible: true,
201
+ defaultCollapsed: true,
202
+ columns: 1,
203
+ fields: [
204
+ { name: 'linkedin', label: 'LinkedIn' },
205
+ { name: 'twitter', label: 'Twitter' },
206
+ ],
207
+ },
208
+ ],
209
+ } as any,
210
+ };
211
+
212
+ /**
213
+ * View with field count badges in section headers
214
+ */
215
+ export const WithFieldCounts: Story = {
216
+ render: renderStory,
217
+ args: {
218
+ type: 'detail-view',
219
+ title: 'Deal Overview',
220
+ objectName: 'Deal',
221
+ resourceId: 'DEAL-456',
222
+ showEdit: true,
223
+ data: {
224
+ name: 'Enterprise License',
225
+ amount: '$120,000',
226
+ stage: 'Negotiation',
227
+ probability: '75%',
228
+ close_date: '2024-03-31',
229
+ owner: 'Sarah Johnson',
230
+ account: 'TechCorp Inc.',
231
+ contact: 'Mike Ross',
232
+ created: '2024-01-15',
233
+ },
234
+ sections: [
235
+ {
236
+ title: 'Deal Information',
237
+ columns: 2,
238
+ fields: [
239
+ { name: 'name', label: 'Deal Name', span: 2 },
240
+ { name: 'amount', label: 'Amount' },
241
+ { name: 'stage', label: 'Stage' },
242
+ { name: 'probability', label: 'Win Probability' },
243
+ { name: 'close_date', label: 'Expected Close' },
244
+ ],
245
+ },
246
+ {
247
+ title: 'Related Records',
248
+ collapsible: true,
249
+ columns: 2,
250
+ fields: [
251
+ { name: 'account', label: 'Account' },
252
+ { name: 'contact', label: 'Primary Contact' },
253
+ { name: 'owner', label: 'Deal Owner' },
254
+ ],
255
+ },
256
+ ],
257
+ } as any,
258
+ };