@object-ui/plugin-list 0.5.1 → 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.
@@ -12,6 +12,8 @@ export interface ViewSwitcherProps {
12
12
  availableViews?: ViewType[];
13
13
  onViewChange: (view: ViewType) => void;
14
14
  className?: string;
15
+ /** Enable animated transitions between views (default: true) */
16
+ animated?: boolean;
15
17
  }
16
18
  export declare const ViewSwitcher: React.FC<ViewSwitcherProps>;
17
19
  //# sourceMappingURL=ViewSwitcher.d.ts.map
@@ -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": "0.5.1",
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": "0.5.0",
29
- "@object-ui/react": "0.5.0",
30
- "@object-ui/types": "0.5.0",
31
- "@object-ui/core": "0.5.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.10",
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
@@ -7,13 +7,14 @@
7
7
  */
8
8
 
9
9
  import * as React from 'react';
10
- import { cn, Button, Input, Popover, PopoverContent, PopoverTrigger, FilterBuilder, SortBuilder } from '@object-ui/components';
10
+ import { cn, Button, Input, Popover, PopoverContent, PopoverTrigger, FilterBuilder, SortBuilder, NavigationOverlay } from '@object-ui/components';
11
11
  import type { SortItem } from '@object-ui/components';
12
- import { Search, SlidersHorizontal, ArrowUpDown, X } from 'lucide-react';
12
+ import { Search, SlidersHorizontal, ArrowUpDown, X, EyeOff, Group, Paintbrush, Ruler } from 'lucide-react';
13
13
  import type { FilterGroup } from '@object-ui/components';
14
14
  import { ViewSwitcher, ViewType } from './ViewSwitcher';
15
- import { SchemaRenderer } from '@object-ui/react';
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;
@@ -22,6 +23,10 @@ export interface ListViewProps {
22
23
  onFilterChange?: (filters: any) => void;
23
24
  onSortChange?: (sort: any) => void;
24
25
  onSearchChange?: (search: string) => void;
26
+ /** Callback when a row/item is clicked (overrides NavigationConfig) */
27
+ onRowClick?: (record: Record<string, unknown>) => void;
28
+ /** Show view type switcher (Grid/Kanban/etc). Default: false (view type is fixed) */
29
+ showViewSwitcher?: boolean;
25
30
  [key: string]: any;
26
31
  }
27
32
 
@@ -65,6 +70,8 @@ export const ListView: React.FC<ListViewProps> = ({
65
70
  onFilterChange,
66
71
  onSortChange,
67
72
  onSearchChange,
73
+ onRowClick,
74
+ showViewSwitcher = false,
68
75
  ...props
69
76
  }) => {
70
77
  // Kernel level default: Ensure viewType is always defined (default to 'grid')
@@ -104,6 +111,16 @@ export const ListView: React.FC<ListViewProps> = ({
104
111
  const [data, setData] = React.useState<any[]>([]);
105
112
  const [loading, setLoading] = React.useState(false);
106
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
+ });
107
124
 
108
125
  const storageKey = React.useMemo(() => {
109
126
  return schema.id
@@ -172,8 +189,8 @@ export const ListView: React.FC<ListViewProps> = ({
172
189
  } else if (results && typeof results === 'object') {
173
190
  if (Array.isArray((results as any).data)) {
174
191
  items = (results as any).data;
175
- } else if (Array.isArray((results as any).value)) {
176
- items = (results as any).value;
192
+ } else if (Array.isArray((results as any).records)) {
193
+ items = (results as any).records;
177
194
  }
178
195
  }
179
196
 
@@ -190,7 +207,7 @@ export const ListView: React.FC<ListViewProps> = ({
190
207
  fetchData();
191
208
 
192
209
  return () => { isMounted = false; };
193
- }, [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
194
211
 
195
212
  // Available view types based on schema configuration
196
213
  const availableViews = React.useMemo(() => {
@@ -272,6 +289,14 @@ export const ListView: React.FC<ListViewProps> = ({
272
289
  onSearchChange?.(value);
273
290
  }, [onSearchChange]);
274
291
 
292
+ // --- NavigationConfig support ---
293
+ const navigation = useNavigationOverlay({
294
+ navigation: schema.navigation,
295
+ objectName: schema.objectName,
296
+ onNavigate: schema.onNavigate,
297
+ onRowClick,
298
+ });
299
+
275
300
  // Generate the appropriate view component schema
276
301
  const viewComponentSchema = React.useMemo(() => {
277
302
  const baseProps = {
@@ -282,6 +307,8 @@ export const ListView: React.FC<ListViewProps> = ({
282
307
  className: "h-full w-full",
283
308
  // Disable internal controls that clash with ListView toolbar
284
309
  showSearch: false,
310
+ // Pass navigation click handler to child views
311
+ onRowClick: navigation.handleClick,
285
312
  };
286
313
 
287
314
  switch (currentView) {
@@ -374,124 +401,189 @@ export const ListView: React.FC<ListViewProps> = ({
374
401
  }));
375
402
  }, [objectDef, schema.fields]);
376
403
 
404
+ const [searchExpanded, setSearchExpanded] = React.useState(false);
405
+
377
406
  return (
378
- <div className={cn('flex flex-col h-full bg-background', className)}>
379
- {/* Airtable-style Toolbar */}
380
- <div className="border-b px-4 py-2 flex items-center justify-between gap-4 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
381
- <div className="flex items-center gap-2 flex-1 overflow-hidden">
382
- {/* View Switcher on the Left */}
383
- <div className="flex items-center pr-2 border-r mr-2">
384
- <ViewSwitcher
385
- currentView={currentView}
386
- availableViews={availableViews}
387
- onViewChange={handleViewChange}
388
- />
389
- </div>
390
-
391
- {/* Action Tools */}
392
- <div className="flex items-center gap-1">
393
- <Popover open={showFilters} onOpenChange={setShowFilters}>
394
- <PopoverTrigger asChild>
395
- <Button
396
- variant={hasFilters ? "secondary" : "ghost"}
397
- size="sm"
398
- className={cn(
399
- "h-8 px-2 lg:px-3 text-muted-foreground hover:text-primary",
400
- hasFilters && "text-primary bg-secondary/50"
401
- )}
402
- >
403
- <SlidersHorizontal className="h-4 w-4 mr-2" />
404
- <span className="hidden lg:inline">Filter</span>
405
- {hasFilters && (
406
- <span className="ml-1.5 flex h-4 w-4 items-center justify-center rounded-full bg-primary/10 text-[10px] font-medium text-primary">
407
- {currentFilters.conditions?.length || 0}
408
- </span>
409
- )}
410
- </Button>
411
- </PopoverTrigger>
412
- <PopoverContent align="start" className="w-[600px] p-4">
413
- <div className="space-y-4">
414
- <div className="flex items-center justify-between border-b pb-2">
415
- <h4 className="font-medium text-sm">Filter Records</h4>
416
- </div>
417
- <FilterBuilder
418
- fields={filterFields}
419
- value={currentFilters}
420
- onChange={(newFilters) => {
421
- console.log('Filter Changed:', newFilters);
422
- setCurrentFilters(newFilters);
423
- // Convert FilterBuilder format to OData $filter string if needed
424
- // For now we just update state and notify listener
425
- // In a real app, this would likely build an OData string
426
- if (onFilterChange) onFilterChange(newFilters);
427
- }}
428
- />
429
- </div>
430
- </PopoverContent>
431
- </Popover>
432
-
433
- <Popover open={showSort} onOpenChange={setShowSort}>
434
- <PopoverTrigger asChild>
435
- <Button
436
- variant={currentSort.length > 0 ? "secondary" : "ghost"}
437
- size="sm"
438
- className={cn(
439
- "h-8 px-2 lg:px-3 text-muted-foreground hover:text-primary",
440
- currentSort.length > 0 && "text-primary bg-secondary/50"
441
- )}
442
- >
443
- <ArrowUpDown className="h-4 w-4 mr-2" />
444
- <span className="hidden lg:inline">Sort</span>
445
- {currentSort.length > 0 && (
446
- <span className="ml-1.5 flex h-4 w-4 items-center justify-center rounded-full bg-primary/10 text-[10px] font-medium text-primary">
447
- {currentSort.length}
448
- </span>
449
- )}
450
- </Button>
451
- </PopoverTrigger>
452
- <PopoverContent align="start" className="w-[600px] p-4">
453
- <div className="space-y-4">
454
- <div className="flex items-center justify-between border-b pb-2">
455
- <h4 className="font-medium text-sm">Sort Records</h4>
456
- </div>
457
- <SortBuilder
458
- fields={filterFields}
459
- value={currentSort}
460
- onChange={(newSort) => {
461
- console.log('Sort Changed:', newSort);
462
- setCurrentSort(newSort);
463
- if (onSortChange) onSortChange(newSort);
464
- }}
465
- />
466
- </div>
467
- </PopoverContent>
468
- </Popover>
469
-
470
- {/* Future: Group, Color, Height */}
471
- </div>
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
+ )}
416
+ {/* Airtable-style Toolbar — Row 1: View tabs */}
417
+ {showViewSwitcher && (
418
+ <div className="border-b px-4 py-1 flex items-center bg-background">
419
+ <ViewSwitcher
420
+ currentView={currentView}
421
+ availableViews={availableViews}
422
+ onViewChange={handleViewChange}
423
+ />
472
424
  </div>
425
+ )}
426
+
427
+ {/* Airtable-style Toolbar — Row 2: Tool buttons */}
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">
430
+ {/* Hide Fields */}
431
+ <Button
432
+ variant="ghost"
433
+ size="sm"
434
+ className="h-7 px-2 text-muted-foreground hover:text-primary text-xs"
435
+ disabled
436
+ >
437
+ <EyeOff className="h-3.5 w-3.5 mr-1.5" />
438
+ <span className="hidden sm:inline">Hide fields</span>
439
+ </Button>
473
440
 
474
- {/* Right Actions: Search + New */}
475
- <div className="flex items-center gap-2">
476
- <div className="relative w-40 lg:w-64 transition-all">
477
- <Search className="absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
478
- <Input
479
- placeholder="Find..."
480
- value={searchTerm}
481
- onChange={(e) => handleSearchChange(e.target.value)}
482
- className="pl-8 h-8 text-sm bg-muted/50 border-transparent hover:bg-muted focus:bg-background focus:border-input transition-colors"
441
+ {/* Filter */}
442
+ <Popover open={showFilters} onOpenChange={setShowFilters}>
443
+ <PopoverTrigger asChild>
444
+ <Button
445
+ variant="ghost"
446
+ size="sm"
447
+ className={cn(
448
+ "h-7 px-2 text-muted-foreground hover:text-primary text-xs",
449
+ hasFilters && "text-primary"
450
+ )}
451
+ >
452
+ <SlidersHorizontal className="h-3.5 w-3.5 mr-1.5" />
453
+ <span className="hidden sm:inline">Filter</span>
454
+ {hasFilters && (
455
+ <span className="ml-1 flex h-4 min-w-[16px] items-center justify-center rounded-full bg-primary/10 text-[10px] font-medium text-primary">
456
+ {currentFilters.conditions?.length || 0}
457
+ </span>
458
+ )}
459
+ </Button>
460
+ </PopoverTrigger>
461
+ <PopoverContent align="start" className="w-[calc(100vw-2rem)] sm:w-[600px] max-w-[600px] p-3 sm:p-4">
462
+ <div className="space-y-4">
463
+ <div className="flex items-center justify-between border-b pb-2">
464
+ <h4 className="font-medium text-sm">Filter Records</h4>
465
+ </div>
466
+ <FilterBuilder
467
+ fields={filterFields}
468
+ value={currentFilters}
469
+ onChange={(newFilters) => {
470
+ setCurrentFilters(newFilters);
471
+ if (onFilterChange) onFilterChange(newFilters);
472
+ }}
483
473
  />
484
- {searchTerm && (
485
- <Button
486
- variant="ghost"
487
- size="sm"
488
- className="absolute right-1 top-1/2 -translate-y-1/2 h-6 w-6 p-0 hover:bg-muted-foreground/20"
489
- onClick={() => handleSearchChange('')}
490
- >
491
- <X className="h-3 w-3" />
492
- </Button>
474
+ </div>
475
+ </PopoverContent>
476
+ </Popover>
477
+
478
+ {/* Group */}
479
+ <Button
480
+ variant="ghost"
481
+ size="sm"
482
+ className="h-7 px-2 text-muted-foreground hover:text-primary text-xs"
483
+ disabled
484
+ >
485
+ <Group className="h-3.5 w-3.5 mr-1.5" />
486
+ <span className="hidden sm:inline">Group</span>
487
+ </Button>
488
+
489
+ {/* Sort */}
490
+ <Popover open={showSort} onOpenChange={setShowSort}>
491
+ <PopoverTrigger asChild>
492
+ <Button
493
+ variant="ghost"
494
+ size="sm"
495
+ className={cn(
496
+ "h-7 px-2 text-muted-foreground hover:text-primary text-xs",
497
+ currentSort.length > 0 && "text-primary"
498
+ )}
499
+ >
500
+ <ArrowUpDown className="h-3.5 w-3.5 mr-1.5" />
501
+ <span className="hidden sm:inline">Sort</span>
502
+ {currentSort.length > 0 && (
503
+ <span className="ml-1 flex h-4 min-w-[16px] items-center justify-center rounded-full bg-primary/10 text-[10px] font-medium text-primary">
504
+ {currentSort.length}
505
+ </span>
493
506
  )}
494
- </div>
507
+ </Button>
508
+ </PopoverTrigger>
509
+ <PopoverContent align="start" className="w-[calc(100vw-2rem)] sm:w-[600px] max-w-[600px] p-3 sm:p-4">
510
+ <div className="space-y-4">
511
+ <div className="flex items-center justify-between border-b pb-2">
512
+ <h4 className="font-medium text-sm">Sort Records</h4>
513
+ </div>
514
+ <SortBuilder
515
+ fields={filterFields}
516
+ value={currentSort}
517
+ onChange={(newSort) => {
518
+ setCurrentSort(newSort);
519
+ if (onSortChange) onSortChange(newSort);
520
+ }}
521
+ />
522
+ </div>
523
+ </PopoverContent>
524
+ </Popover>
525
+
526
+ {/* Color */}
527
+ <Button
528
+ variant="ghost"
529
+ size="sm"
530
+ className="h-7 px-2 text-muted-foreground hover:text-primary text-xs"
531
+ disabled
532
+ >
533
+ <Paintbrush className="h-3.5 w-3.5 mr-1.5" />
534
+ <span className="hidden sm:inline">Color</span>
535
+ </Button>
536
+
537
+ {/* Row Height */}
538
+ <Button
539
+ variant="ghost"
540
+ size="sm"
541
+ className="h-7 px-2 text-muted-foreground hover:text-primary text-xs hidden lg:flex"
542
+ disabled
543
+ >
544
+ <Ruler className="h-3.5 w-3.5 mr-1.5" />
545
+ <span className="hidden sm:inline">Row height</span>
546
+ </Button>
547
+ </div>
548
+
549
+ {/* Right: Search */}
550
+ <div className="flex items-center gap-1">
551
+ {searchExpanded ? (
552
+ <div className="relative w-36 sm:w-48 lg:w-64">
553
+ <Search className="absolute left-2 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
554
+ <Input
555
+ placeholder="Find..."
556
+ value={searchTerm}
557
+ onChange={(e) => handleSearchChange(e.target.value)}
558
+ className="pl-7 h-7 text-xs bg-muted/50 border-transparent hover:bg-muted focus:bg-background focus:border-input transition-colors"
559
+ autoFocus
560
+ onBlur={() => {
561
+ if (!searchTerm) setSearchExpanded(false);
562
+ }}
563
+ />
564
+ <Button
565
+ variant="ghost"
566
+ size="sm"
567
+ className="absolute right-0.5 top-1/2 -translate-y-1/2 h-5 w-5 p-0 hover:bg-muted-foreground/20"
568
+ onClick={() => {
569
+ handleSearchChange('');
570
+ setSearchExpanded(false);
571
+ }}
572
+ >
573
+ <X className="h-3 w-3" />
574
+ </Button>
575
+ </div>
576
+ ) : (
577
+ <Button
578
+ variant="ghost"
579
+ size="sm"
580
+ className="h-7 px-2 text-muted-foreground hover:text-primary text-xs"
581
+ onClick={() => setSearchExpanded(true)}
582
+ >
583
+ <Search className="h-3.5 w-3.5 mr-1.5" />
584
+ <span className="hidden sm:inline">Search</span>
585
+ </Button>
586
+ )}
495
587
  </div>
496
588
  </div>
497
589
 
@@ -499,7 +591,7 @@ export const ListView: React.FC<ListViewProps> = ({
499
591
  {/* Filters Panel - Removed as it is now in Popover */}
500
592
 
501
593
  {/* View Content */}
502
- <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">
503
595
  <SchemaRenderer
504
596
  schema={viewComponentSchema}
505
597
  {...props}
@@ -507,6 +599,33 @@ export const ListView: React.FC<ListViewProps> = ({
507
599
  loading={loading}
508
600
  />
509
601
  </div>
602
+
603
+ {/* Navigation Overlay (drawer/modal/popover) */}
604
+ {navigation.isOverlay && (
605
+ <NavigationOverlay
606
+ {...navigation}
607
+ title={
608
+ schema.label
609
+ ? `${schema.label} Detail`
610
+ : schema.objectName
611
+ ? `${schema.objectName.charAt(0).toUpperCase() + schema.objectName.slice(1)} Detail`
612
+ : 'Record Detail'
613
+ }
614
+ >
615
+ {(record) => (
616
+ <div className="space-y-3">
617
+ {Object.entries(record).map(([key, value]) => (
618
+ <div key={key} className="flex flex-col">
619
+ <span className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
620
+ {key.replace(/_/g, ' ')}
621
+ </span>
622
+ <span className="text-sm">{String(value ?? '—')}</span>
623
+ </div>
624
+ ))}
625
+ </div>
626
+ )}
627
+ </NavigationOverlay>
628
+ )}
510
629
  </div>
511
630
  );
512
631
  };