@object-ui/plugin-list 2.0.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.
@@ -1 +1 @@
1
- {"version":3,"file":"ViewSwitcher.d.ts","sourceRoot":"","sources":["../../src/ViewSwitcher.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAY/B,MAAM,MAAM,QAAQ,GAChB,MAAM,GACN,QAAQ,GACR,SAAS,GACT,UAAU,GACV,UAAU,GACV,OAAO,GACP,KAAK,CAAC;AAEV,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,QAAQ,CAAC;IACtB,cAAc,CAAC,EAAE,QAAQ,EAAE,CAAC;IAC5B,YAAY,EAAE,CAAC,IAAI,EAAE,QAAQ,KAAK,IAAI,CAAC;IACvC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAsBD,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CAmCpD,CAAC"}
1
+ {"version":3,"file":"ViewSwitcher.d.ts","sourceRoot":"","sources":["../../src/ViewSwitcher.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAY/B,MAAM,MAAM,QAAQ,GAChB,MAAM,GACN,QAAQ,GACR,SAAS,GACT,UAAU,GACV,UAAU,GACV,OAAO,GACP,KAAK,CAAC;AAEV,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,QAAQ,CAAC;IACtB,cAAc,CAAC,EAAE,QAAQ,EAAE,CAAC;IAC5B,YAAY,EAAE,CAAC,IAAI,EAAE,QAAQ,KAAK,IAAI,CAAC;IACvC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gEAAgE;IAChE,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAsBD,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CAsDpD,CAAC"}
@@ -3,5 +3,6 @@ import { ViewSwitcher } from './ViewSwitcher';
3
3
  import { ObjectGallery } from './ObjectGallery';
4
4
  export { ListView, ViewSwitcher, ObjectGallery };
5
5
  export type { ListViewProps } from './ListView';
6
+ export type { ObjectGalleryProps } from './ObjectGallery';
6
7
  export type { ViewSwitcherProps, ViewType } from './ViewSwitcher';
7
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAGhD,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,aAAa,EAAE,CAAC;AACjD,YAAY,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAChD,YAAY,EAAE,iBAAiB,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAEhD,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,aAAa,EAAE,CAAC;AACjD,YAAY,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAChD,YAAY,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAC1D,YAAY,EAAE,iBAAiB,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@object-ui/plugin-list",
3
- "version": "2.0.0",
3
+ "version": "3.0.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "ListView plugin for Object UI - unified view component with view type switching",
@@ -25,19 +25,20 @@
25
25
  },
26
26
  "dependencies": {
27
27
  "lucide-react": "^0.563.0",
28
- "@object-ui/components": "2.0.0",
29
- "@object-ui/core": "2.0.0",
30
- "@object-ui/react": "2.0.0",
31
- "@object-ui/types": "2.0.0"
28
+ "@object-ui/components": "3.0.0",
29
+ "@object-ui/core": "3.0.0",
30
+ "@object-ui/mobile": "3.0.0",
31
+ "@object-ui/react": "3.0.0",
32
+ "@object-ui/types": "3.0.0"
32
33
  },
33
34
  "peerDependencies": {
34
35
  "react": "^18.0.0 || ^19.0.0",
35
36
  "react-dom": "^18.0.0 || ^19.0.0"
36
37
  },
37
38
  "devDependencies": {
38
- "@types/react": "^19.2.13",
39
- "@types/react-dom": "^19.2.3",
40
- "@vitejs/plugin-react": "^5.1.3",
39
+ "@types/react": "19.2.13",
40
+ "@types/react-dom": "19.2.3",
41
+ "@vitejs/plugin-react": "^5.1.4",
41
42
  "typescript": "^5.9.3",
42
43
  "vite": "^7.3.1",
43
44
  "vite-plugin-dts": "^4.5.4",
@@ -0,0 +1,64 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { SchemaRenderer } from '@object-ui/react';
3
+ import type { BaseSchema } from '@object-ui/types';
4
+
5
+ const meta = {
6
+ title: 'Plugins/ListView',
7
+ component: SchemaRenderer,
8
+ parameters: {
9
+ layout: 'padded',
10
+ },
11
+ tags: ['autodocs'],
12
+ argTypes: {
13
+ schema: { table: { disable: true } },
14
+ },
15
+ } satisfies Meta<any>;
16
+
17
+ export default meta;
18
+ type Story = StoryObj<typeof meta>;
19
+
20
+ const renderStory = (args: any) => <SchemaRenderer schema={args as unknown as BaseSchema} />;
21
+
22
+ export const Default: Story = {
23
+ render: renderStory,
24
+ args: {
25
+ type: 'list-view',
26
+ objectName: 'contacts',
27
+ viewType: 'grid',
28
+ fields: ['name', 'email', 'phone', 'company'],
29
+ sort: [{ field: 'name', order: 'asc' }],
30
+ } as any,
31
+ };
32
+
33
+ export const KanbanView: Story = {
34
+ render: renderStory,
35
+ args: {
36
+ type: 'list-view',
37
+ objectName: 'deals',
38
+ viewType: 'kanban',
39
+ fields: ['name', 'amount', 'stage', 'close_date'],
40
+ options: {
41
+ kanban: {
42
+ groupField: 'stage',
43
+ titleField: 'name',
44
+ cardFields: ['amount', 'close_date'],
45
+ },
46
+ },
47
+ } as any,
48
+ };
49
+
50
+ export const WithFilters: Story = {
51
+ render: renderStory,
52
+ args: {
53
+ type: 'list-view',
54
+ objectName: 'opportunities',
55
+ viewType: 'grid',
56
+ fields: ['name', 'amount', 'stage', 'owner', 'close_date'],
57
+ filters: [
58
+ ['stage', '=', 'Prospecting'],
59
+ 'OR',
60
+ ['stage', '=', 'Qualification'],
61
+ ],
62
+ sort: [{ field: 'amount', order: 'desc' }],
63
+ } as any,
64
+ };
package/src/ListView.tsx CHANGED
@@ -14,6 +14,7 @@ import type { FilterGroup } from '@object-ui/components';
14
14
  import { ViewSwitcher, ViewType } from './ViewSwitcher';
15
15
  import { SchemaRenderer, useNavigationOverlay } from '@object-ui/react';
16
16
  import type { ListViewSchema } from '@object-ui/types';
17
+ import { usePullToRefresh } from '@object-ui/mobile';
17
18
 
18
19
  export interface ListViewProps {
19
20
  schema: ListViewSchema;
@@ -110,6 +111,16 @@ export const ListView: React.FC<ListViewProps> = ({
110
111
  const [data, setData] = React.useState<any[]>([]);
111
112
  const [loading, setLoading] = React.useState(false);
112
113
  const [objectDef, setObjectDef] = React.useState<any>(null);
114
+ const [refreshKey, setRefreshKey] = React.useState(0);
115
+
116
+ const handlePullRefresh = React.useCallback(async () => {
117
+ setRefreshKey(k => k + 1);
118
+ }, []);
119
+
120
+ const { ref: pullRef, isRefreshing, pullDistance } = usePullToRefresh<HTMLDivElement>({
121
+ onRefresh: handlePullRefresh,
122
+ enabled: !!dataSource && !!schema.objectName,
123
+ });
113
124
 
114
125
  const storageKey = React.useMemo(() => {
115
126
  return schema.id
@@ -178,8 +189,8 @@ export const ListView: React.FC<ListViewProps> = ({
178
189
  } else if (results && typeof results === 'object') {
179
190
  if (Array.isArray((results as any).data)) {
180
191
  items = (results as any).data;
181
- } else if (Array.isArray((results as any).value)) {
182
- items = (results as any).value;
192
+ } else if (Array.isArray((results as any).records)) {
193
+ items = (results as any).records;
183
194
  }
184
195
  }
185
196
 
@@ -196,7 +207,7 @@ export const ListView: React.FC<ListViewProps> = ({
196
207
  fetchData();
197
208
 
198
209
  return () => { isMounted = false; };
199
- }, [schema.objectName, dataSource, schema.filters, currentSort, currentFilters]); // Re-fetch on filter/sort change
210
+ }, [schema.objectName, dataSource, schema.filters, currentSort, currentFilters, refreshKey]); // Re-fetch on filter/sort change
200
211
 
201
212
  // Available view types based on schema configuration
202
213
  const availableViews = React.useMemo(() => {
@@ -393,7 +404,15 @@ export const ListView: React.FC<ListViewProps> = ({
393
404
  const [searchExpanded, setSearchExpanded] = React.useState(false);
394
405
 
395
406
  return (
396
- <div className={cn('flex flex-col h-full bg-background', className)}>
407
+ <div ref={pullRef} className={cn('flex flex-col h-full bg-background relative', className)}>
408
+ {pullDistance > 0 && (
409
+ <div
410
+ className="flex items-center justify-center text-xs text-muted-foreground"
411
+ style={{ height: pullDistance }}
412
+ >
413
+ {isRefreshing ? 'Refreshing…' : 'Pull to refresh'}
414
+ </div>
415
+ )}
397
416
  {/* Airtable-style Toolbar — Row 1: View tabs */}
398
417
  {showViewSwitcher && (
399
418
  <div className="border-b px-4 py-1 flex items-center bg-background">
@@ -406,8 +425,8 @@ export const ListView: React.FC<ListViewProps> = ({
406
425
  )}
407
426
 
408
427
  {/* Airtable-style Toolbar — Row 2: Tool buttons */}
409
- <div className="border-b px-4 py-1 flex items-center justify-between gap-2 bg-background">
410
- <div className="flex items-center gap-0.5 overflow-hidden">
428
+ <div className="border-b px-2 sm:px-4 py-1 flex items-center justify-between gap-1 sm:gap-2 bg-background">
429
+ <div className="flex items-center gap-0.5 overflow-hidden flex-1 min-w-0">
411
430
  {/* Hide Fields */}
412
431
  <Button
413
432
  variant="ghost"
@@ -439,7 +458,7 @@ export const ListView: React.FC<ListViewProps> = ({
439
458
  )}
440
459
  </Button>
441
460
  </PopoverTrigger>
442
- <PopoverContent align="start" className="w-[600px] p-4">
461
+ <PopoverContent align="start" className="w-[calc(100vw-2rem)] sm:w-[600px] max-w-[600px] p-3 sm:p-4">
443
462
  <div className="space-y-4">
444
463
  <div className="flex items-center justify-between border-b pb-2">
445
464
  <h4 className="font-medium text-sm">Filter Records</h4>
@@ -487,7 +506,7 @@ export const ListView: React.FC<ListViewProps> = ({
487
506
  )}
488
507
  </Button>
489
508
  </PopoverTrigger>
490
- <PopoverContent align="start" className="w-[600px] p-4">
509
+ <PopoverContent align="start" className="w-[calc(100vw-2rem)] sm:w-[600px] max-w-[600px] p-3 sm:p-4">
491
510
  <div className="space-y-4">
492
511
  <div className="flex items-center justify-between border-b pb-2">
493
512
  <h4 className="font-medium text-sm">Sort Records</h4>
@@ -530,7 +549,7 @@ export const ListView: React.FC<ListViewProps> = ({
530
549
  {/* Right: Search */}
531
550
  <div className="flex items-center gap-1">
532
551
  {searchExpanded ? (
533
- <div className="relative w-48 lg:w-64">
552
+ <div className="relative w-36 sm:w-48 lg:w-64">
534
553
  <Search className="absolute left-2 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
535
554
  <Input
536
555
  placeholder="Find..."
@@ -572,7 +591,7 @@ export const ListView: React.FC<ListViewProps> = ({
572
591
  {/* Filters Panel - Removed as it is now in Popover */}
573
592
 
574
593
  {/* View Content */}
575
- <div className="flex-1 min-h-0 bg-background relative overflow-hidden">
594
+ <div key={currentView} className="flex-1 min-h-0 bg-background relative overflow-hidden animate-in fade-in-0 duration-200">
576
595
  <SchemaRenderer
577
596
  schema={viewComponentSchema}
578
597
  {...props}
@@ -1,27 +1,69 @@
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
+ */
1
8
 
2
9
  import React, { useState, useEffect } from 'react';
3
10
  import { useDataScope, useSchemaContext } from '@object-ui/react';
4
11
  import { ComponentRegistry } from '@object-ui/core';
12
+ import { cn, Card, CardContent } from '@object-ui/components';
13
+ import type { GalleryConfig } from '@object-ui/types';
5
14
 
6
- // Utility for class merging (assuming it's available in plugin context,
7
- // usually provided by @object-ui/components or utils, but here I'll just use string concat if not imported)
8
- // Actually @object-ui/components exports cn
9
- import { cn } from '@object-ui/components';
15
+ export interface ObjectGalleryProps {
16
+ schema: {
17
+ objectName?: string;
18
+ bind?: string;
19
+ filter?: unknown;
20
+ data?: Record<string, unknown>[];
21
+ className?: string;
22
+ gallery?: GalleryConfig;
23
+ /** @deprecated Use gallery.coverField instead */
24
+ imageField?: string;
25
+ /** @deprecated Use gallery.titleField instead */
26
+ titleField?: string;
27
+ subtitleField?: string;
28
+ };
29
+ data?: Record<string, unknown>[];
30
+ dataSource?: { find: (name: string, query: unknown) => Promise<unknown> };
31
+ onCardClick?: (record: Record<string, unknown>) => void;
32
+ }
10
33
 
11
- export const ObjectGallery = (props: any) => {
34
+ const GRID_CLASSES: Record<NonNullable<GalleryConfig['cardSize']>, string> = {
35
+ small: 'grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6',
36
+ medium: 'grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4',
37
+ large: 'grid-cols-1 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
38
+ };
39
+
40
+ const ASPECT_CLASSES: Record<NonNullable<GalleryConfig['cardSize']>, string> = {
41
+ small: 'aspect-square',
42
+ medium: 'aspect-[4/3]',
43
+ large: 'aspect-[16/10]',
44
+ };
45
+
46
+ export const ObjectGallery: React.FC<ObjectGalleryProps> = (props) => {
12
47
  const { schema } = props;
13
48
  const context = useSchemaContext();
14
49
  const dataSource = props.dataSource || context.dataSource;
15
50
  const boundData = useDataScope(schema.bind);
16
51
 
17
- const [fetchedData, setFetchedData] = useState<any[]>([]);
52
+ const [fetchedData, setFetchedData] = useState<Record<string, unknown>[]>([]);
18
53
  const [loading, setLoading] = useState(false);
19
54
 
55
+ // Resolve GalleryConfig with backwards-compatible fallbacks
56
+ const gallery = schema.gallery;
57
+ const coverField = gallery?.coverField ?? schema.imageField ?? 'image';
58
+ const coverFit = gallery?.coverFit ?? 'cover';
59
+ const cardSize = gallery?.cardSize ?? 'medium';
60
+ const titleField = gallery?.titleField ?? schema.titleField ?? 'name';
61
+ const visibleFields = gallery?.visibleFields;
62
+
20
63
  useEffect(() => {
21
64
  let isMounted = true;
22
-
23
- // Use data prop if available (from ListView)
24
- if ((props.data && Array.isArray(props.data))) {
65
+
66
+ if (props.data && Array.isArray(props.data)) {
25
67
  setFetchedData(props.data);
26
68
  return;
27
69
  }
@@ -30,22 +72,22 @@ export const ObjectGallery = (props: any) => {
30
72
  if (!dataSource || !schema.objectName) return;
31
73
  if (isMounted) setLoading(true);
32
74
  try {
33
- // Apply filtering?
34
75
  const results = await dataSource.find(schema.objectName, {
35
- $filter: schema.filter
76
+ $filter: schema.filter,
36
77
  });
37
-
38
- let data: any[] = [];
78
+
79
+ let data: Record<string, unknown>[] = [];
39
80
  if (Array.isArray(results)) {
40
81
  data = results;
41
82
  } else if (results && typeof results === 'object') {
42
- if (Array.isArray((results as any).value)) {
43
- data = (results as any).value;
44
- } else if (Array.isArray((results as any).data)) {
45
- data = (results as any).data;
83
+ const r = results as Record<string, unknown>;
84
+ if (Array.isArray(r.records)) {
85
+ data = r.records as Record<string, unknown>[];
86
+ } else if (Array.isArray(r.data)) {
87
+ data = r.data as Record<string, unknown>[];
46
88
  }
47
89
  }
48
-
90
+
49
91
  if (isMounted) {
50
92
  setFetchedData(data);
51
93
  }
@@ -55,51 +97,78 @@ export const ObjectGallery = (props: any) => {
55
97
  if (isMounted) setLoading(false);
56
98
  }
57
99
  };
58
-
100
+
59
101
  if (schema.objectName && !boundData && !schema.data && !props.data) {
60
102
  fetchData();
61
103
  }
62
104
  return () => { isMounted = false; };
63
- }, [schema.objectName, dataSource, boundData, schema.data, schema.filter, props.data]);
105
+ }, [schema.objectName, dataSource, boundData, schema.data, schema.filter, props.data]);
64
106
 
65
- const items = props.data || boundData || schema.data || fetchedData || [];
66
-
67
- // Config
68
- const imageField = schema.imageField || 'image';
69
- const titleField = schema.titleField || 'name';
70
- const subtitleField = schema.subtitleField;
107
+ const items: Record<string, unknown>[] = props.data || boundData || schema.data || fetchedData || [];
71
108
 
72
109
  if (loading && !items.length) return <div className="p-4 text-sm text-muted-foreground">Loading Gallery...</div>;
73
110
  if (!items.length) return <div className="p-4 text-sm text-muted-foreground">No items to display</div>;
74
111
 
75
112
  return (
76
- <div className={cn("grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4 p-4", schema.className)}>
77
- {items.map((item: any, i: number) => (
78
- <div key={item._id || i} className="group relative border rounded-lg overflow-hidden bg-card text-card-foreground shadow-sm hover:shadow-md transition-all">
79
- <div className="aspect-square w-full overflow-hidden bg-muted relative">
80
- {item[imageField] ? (
81
- <img
82
- src={item[imageField]}
83
- alt={item[titleField]}
84
- className="h-full w-full object-cover transition-transform group-hover:scale-105"
85
- onError={(e) => {
86
- (e.target as HTMLImageElement).src = `https://placehold.co/400x400?text=${encodeURIComponent(item[titleField]?.[0] || '?')}`;
87
- }}
88
- />
89
- ) : (
90
- <div className="flex h-full w-full items-center justify-center bg-secondary/50 text-muted-foreground">
91
- <span className="text-4xl font-light opacity-20">{item[titleField]?.[0]?.toUpperCase()}</span>
92
- </div>
93
- )}
94
- </div>
95
- <div className="p-3 border-t">
96
- <h3 className="font-medium truncate text-sm" title={item[titleField]}>{item[titleField] || 'Untitled'}</h3>
97
- {subtitleField && (
98
- <p className="text-xs text-muted-foreground truncate mt-1">{item[subtitleField]}</p>
113
+ <div
114
+ className={cn('grid gap-4 p-4', GRID_CLASSES[cardSize], schema.className)}
115
+ role="list"
116
+ >
117
+ {items.map((item, i) => {
118
+ const id = (item._id ?? item.id ?? i) as string | number;
119
+ const title = String(item[titleField] ?? 'Untitled');
120
+ const imageUrl = item[coverField] as string | undefined;
121
+
122
+ return (
123
+ <Card
124
+ key={id}
125
+ role="listitem"
126
+ className={cn(
127
+ 'group overflow-hidden transition-all hover:shadow-md',
128
+ props.onCardClick && 'cursor-pointer',
99
129
  )}
100
- </div>
101
- </div>
102
- ))}
130
+ onClick={props.onCardClick ? () => props.onCardClick!(item) : undefined}
131
+ >
132
+ <div className={cn('w-full overflow-hidden bg-muted relative', ASPECT_CLASSES[cardSize])}>
133
+ {imageUrl ? (
134
+ <img
135
+ src={imageUrl}
136
+ alt={title}
137
+ className={cn(
138
+ 'h-full w-full transition-transform group-hover:scale-105',
139
+ coverFit === 'cover' && 'object-cover',
140
+ coverFit === 'contain' && 'object-contain',
141
+ )}
142
+ />
143
+ ) : (
144
+ <div className="flex h-full w-full items-center justify-center bg-secondary/50 text-muted-foreground">
145
+ <span className="text-4xl font-light opacity-20">
146
+ {title[0]?.toUpperCase()}
147
+ </span>
148
+ </div>
149
+ )}
150
+ </div>
151
+ <CardContent className="p-3 border-t">
152
+ <h3 className="font-medium truncate text-sm" title={title}>
153
+ {title}
154
+ </h3>
155
+ {visibleFields && visibleFields.length > 0 && (
156
+ <div className="mt-1 space-y-0.5">
157
+ {visibleFields.map((field) => {
158
+ const value = item[field];
159
+ if (value == null) return null;
160
+ return (
161
+ <p key={field} className="text-xs text-muted-foreground truncate">
162
+ {String(value)}
163
+ </p>
164
+ );
165
+ })}
166
+ </div>
167
+ )}
168
+ </CardContent>
169
+ </Card>
170
+ );
171
+ })}
103
172
  </div>
104
173
  );
105
174
  };
@@ -107,5 +176,5 @@ export const ObjectGallery = (props: any) => {
107
176
  ComponentRegistry.register('object-gallery', ObjectGallery, {
108
177
  namespace: 'plugin-list',
109
178
  label: 'Gallery View',
110
- category: 'view'
179
+ category: 'view',
111
180
  });
@@ -32,6 +32,8 @@ export interface ViewSwitcherProps {
32
32
  availableViews?: ViewType[];
33
33
  onViewChange: (view: ViewType) => void;
34
34
  className?: string;
35
+ /** Enable animated transitions between views (default: true) */
36
+ animated?: boolean;
35
37
  }
36
38
 
37
39
  const VIEW_ICONS: Record<ViewType, React.ReactNode> = {
@@ -59,16 +61,35 @@ export const ViewSwitcher: React.FC<ViewSwitcherProps> = ({
59
61
  availableViews = ['grid', 'kanban'],
60
62
  onViewChange,
61
63
  className,
64
+ animated = true,
62
65
  }) => {
66
+ const handleViewChange = React.useCallback(
67
+ (view: ViewType) => {
68
+ if (!animated || view === currentView) {
69
+ onViewChange(view);
70
+ return;
71
+ }
72
+
73
+ if (typeof document !== 'undefined' && 'startViewTransition' in document) {
74
+ (document as Document & {
75
+ startViewTransition: (cb: () => void) => { finished: Promise<void> };
76
+ }).startViewTransition(() => onViewChange(view));
77
+ } else {
78
+ onViewChange(view);
79
+ }
80
+ },
81
+ [animated, currentView, onViewChange],
82
+ );
83
+
63
84
  return (
64
- <div className={cn("flex items-center gap-1 bg-transparent", className)}>
85
+ <div className={cn("flex items-center gap-1 bg-transparent oui-view-switcher", className)}>
65
86
  {availableViews.map((view) => {
66
87
  const isActive = currentView === view;
67
88
  return (
68
89
  <button
69
90
  key={view}
70
91
  type="button"
71
- onClick={() => onViewChange(view)}
92
+ onClick={() => handleViewChange(view)}
72
93
  aria-label={VIEW_LABELS[view]}
73
94
  title={VIEW_LABELS[view]}
74
95
  aria-pressed={isActive}
package/src/index.tsx CHANGED
@@ -10,10 +10,10 @@ import { ComponentRegistry } from '@object-ui/core';
10
10
  import { ListView } from './ListView';
11
11
  import { ViewSwitcher } from './ViewSwitcher';
12
12
  import { ObjectGallery } from './ObjectGallery';
13
- import type { ListViewSchema } from '@object-ui/types';
14
13
 
15
14
  export { ListView, ViewSwitcher, ObjectGallery };
16
15
  export type { ListViewProps } from './ListView';
16
+ export type { ObjectGalleryProps } from './ObjectGallery';
17
17
  export type { ViewSwitcherProps, ViewType } from './ViewSwitcher';
18
18
 
19
19
  // Register ListView component
@@ -1,5 +1,4 @@
1
- import { describe, it, expect, beforeAll } from 'vitest';
2
- import { ComponentRegistry } from '@object-ui/core';
1
+ import { describe, it, expect } from 'vitest';
3
2
  import { ListView } from './index';
4
3
 
5
4
  describe('Plugin List Registration', () => {
package/vitest.config.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  /// <reference types="vitest" />
2
2
  import { defineConfig } from 'vite';
3
3
  import react from '@vitejs/plugin-react';
4
- import path from 'path';
5
4
 
6
5
  export default defineConfig({
7
6
  plugins: [react()],