@object-ui/plugin-detail 3.3.0 → 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 (134) hide show
  1. package/CHANGELOG.md +11 -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-Xuieq0ZI.js → AvatarField-Dy2XGlPz.js} +16 -15
  6. package/dist/{BooleanField-DwfMKknK.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-B0HTmmD7.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-DVTimsc3.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-rRi_P0N0.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-2CnhcWI0.js → SignatureField-B1wh3f5A.js} +18 -17
  31. package/dist/{SliderField-DEpMVXko.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.js +1912 -1728
  40. package/dist/index.umd.cjs +38 -47
  41. package/dist/packages/plugin-detail/src/DetailSection.d.ts.map +1 -1
  42. package/dist/packages/plugin-detail/src/DetailView.d.ts +24 -0
  43. package/dist/packages/plugin-detail/src/DetailView.d.ts.map +1 -1
  44. package/dist/packages/plugin-detail/src/RelatedList.d.ts +8 -0
  45. package/dist/packages/plugin-detail/src/RelatedList.d.ts.map +1 -1
  46. package/dist/packages/plugin-detail/src/useDetailTranslation.d.ts.map +1 -1
  47. package/dist/plugin-detail.css +1 -2
  48. package/dist/rolldown-runtime-DnwLefa7.js +23 -0
  49. package/dist/{src-C56Ly5uG.js → src-DyUKLvMN.js} +18271 -26636
  50. package/dist/{useFieldTranslation-CkxqyB82.js → useFieldTranslation-BRgjC1oq.js} +1 -1
  51. package/package.json +33 -11
  52. package/.turbo/turbo-build.log +0 -64
  53. package/dist/AddressField-CDLSeyNx.js +0 -93
  54. package/dist/AutoNumberField-CtE7suf5.js +0 -14
  55. package/dist/CodeField-CfwgRxx2.js +0 -22
  56. package/dist/ColorField-YKHA7dBD.js +0 -37
  57. package/dist/CurrencyField-tvS3fPAF.js +0 -51
  58. package/dist/DateField-BKqXpkOh.js +0 -21
  59. package/dist/DateTimeField-CR-nJCE7.js +0 -32
  60. package/dist/EmailField-CgvW1Qal.js +0 -28
  61. package/dist/FileField-BVAme2ML.js +0 -151
  62. package/dist/FormulaField-DamJ2VaG.js +0 -14
  63. package/dist/GeolocationField-C99z7ZBM.js +0 -113
  64. package/dist/GridField-C9JbpTx_.js +0 -51
  65. package/dist/ImageField-CDANtgVV.js +0 -75
  66. package/dist/LocationField-ZSyZ0O-h.js +0 -35
  67. package/dist/LookupField-B3hQJt95.js +0 -903
  68. package/dist/LookupField-D00z6gn_.js +0 -2
  69. package/dist/NumberField-DL2QAL7X.js +0 -26
  70. package/dist/ObjectField-JYvUnuRO.js +0 -52
  71. package/dist/PercentField-DjR6BSpw.js +0 -63
  72. package/dist/PhoneField-CX1JL-jp.js +0 -28
  73. package/dist/QRCodeField-CH_1pU6R.js +0 -72
  74. package/dist/RichTextField-CJqLWlrb.js +0 -32
  75. package/dist/SelectField-DGoDoRM_.js +0 -30
  76. package/dist/SelectField-XBVI50AD.js +0 -2
  77. package/dist/SummaryField-7ch9aqAu.js +0 -19
  78. package/dist/TextAreaField-Cmw1oXcw.js +0 -36
  79. package/dist/TextField-OTLa3p51.js +0 -29
  80. package/dist/TimeField-DKPoNWoR.js +0 -21
  81. package/dist/UrlField-CxbmzP9f.js +0 -33
  82. package/dist/UserField-ChvwUkMK.js +0 -78
  83. package/dist/VectorField-BVClL8Vw.js +0 -36
  84. package/src/ActivityTimeline.tsx +0 -184
  85. package/src/CommentAttachment.tsx +0 -194
  86. package/src/CommentInput.tsx +0 -81
  87. package/src/DetailSection.tsx +0 -340
  88. package/src/DetailTabs.tsx +0 -73
  89. package/src/DetailView.stories.tsx +0 -334
  90. package/src/DetailView.tsx +0 -823
  91. package/src/DiffView.tsx +0 -233
  92. package/src/FieldChangeItem.tsx +0 -46
  93. package/src/HeaderHighlight.tsx +0 -88
  94. package/src/InlineCreateRelated.tsx +0 -291
  95. package/src/MentionAutocomplete.tsx +0 -123
  96. package/src/PointInTimeRestore.tsx +0 -261
  97. package/src/ReactionPicker.tsx +0 -106
  98. package/src/RecordActivityTimeline.tsx +0 -433
  99. package/src/RecordChatterPanel.tsx +0 -209
  100. package/src/RecordComments.tsx +0 -217
  101. package/src/RecordNavigationEnhanced.tsx +0 -213
  102. package/src/RelatedList.tsx +0 -413
  103. package/src/RelationshipGraph.tsx +0 -286
  104. package/src/RichTextCommentInput.tsx +0 -350
  105. package/src/SectionGroup.tsx +0 -101
  106. package/src/SubscriptionToggle.tsx +0 -62
  107. package/src/ThreadedReplies.tsx +0 -163
  108. package/src/__tests__/ActivityTimeline.test.tsx +0 -119
  109. package/src/__tests__/ActivityTimelineFiltering.test.tsx +0 -143
  110. package/src/__tests__/CommentInput.test.tsx +0 -57
  111. package/src/__tests__/DetailSection.test.tsx +0 -490
  112. package/src/__tests__/DetailView.test.tsx +0 -694
  113. package/src/__tests__/FieldChangeItem.test.tsx +0 -119
  114. package/src/__tests__/HeaderHighlight.test.tsx +0 -213
  115. package/src/__tests__/MentionAutocomplete.test.tsx +0 -97
  116. package/src/__tests__/ReactionPicker.test.tsx +0 -113
  117. package/src/__tests__/RecordActivityTimeline.test.tsx +0 -395
  118. package/src/__tests__/RecordChatterPanel.test.tsx +0 -265
  119. package/src/__tests__/RecordComments.test.tsx +0 -96
  120. package/src/__tests__/RecordCommentsPinSearch.test.tsx +0 -133
  121. package/src/__tests__/RelatedList.test.tsx +0 -160
  122. package/src/__tests__/SectionGroup.test.tsx +0 -101
  123. package/src/__tests__/SubscriptionToggle.test.tsx +0 -84
  124. package/src/__tests__/ThreadedReplies.test.tsx +0 -212
  125. package/src/__tests__/autoLayout.test.ts +0 -228
  126. package/src/__tests__/phase12-features.test.tsx +0 -583
  127. package/src/__tests__/roadmap-features.test.tsx +0 -478
  128. package/src/autoLayout.ts +0 -128
  129. package/src/index.tsx +0 -149
  130. package/src/useDetailTranslation.ts +0 -183
  131. package/tsconfig.json +0 -18
  132. package/vite.config.ts +0 -57
  133. package/vitest.config.ts +0 -13
  134. package/vitest.setup.ts +0 -1
@@ -1,217 +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 { cn, Button, Card, CardHeader, CardTitle, CardContent } from '@object-ui/components';
11
- import { MessageSquare, Send, Pin, Search, X } from 'lucide-react';
12
- import type { CommentEntry } from '@object-ui/types';
13
- import { useDetailTranslation } from './useDetailTranslation';
14
-
15
- export interface RecordCommentsProps {
16
- comments: CommentEntry[];
17
- onAddComment?: (text: string) => void | Promise<void>;
18
- /** Callback to toggle pin/star on a comment */
19
- onTogglePin?: (commentId: string | number) => void;
20
- /** Enable search input for filtering comments */
21
- searchable?: boolean;
22
- className?: string;
23
- }
24
-
25
- /**
26
- * Format a timestamp string into a human-readable relative time or date string.
27
- */
28
- function formatTimestamp(timestamp: string): string {
29
- try {
30
- const date = new Date(timestamp);
31
- const now = new Date();
32
- const diffMs = now.getTime() - date.getTime();
33
- const diffMins = Math.floor(diffMs / 60000);
34
-
35
- if (diffMins < 1) return 'just now';
36
- if (diffMins < 60) return `${diffMins}m ago`;
37
- const diffHours = Math.floor(diffMins / 60);
38
- if (diffHours < 24) return `${diffHours}h ago`;
39
- const diffDays = Math.floor(diffHours / 24);
40
- if (diffDays < 7) return `${diffDays}d ago`;
41
- return date.toLocaleDateString();
42
- } catch {
43
- return timestamp;
44
- }
45
- }
46
-
47
- export const RecordComments: React.FC<RecordCommentsProps> = ({
48
- comments,
49
- onAddComment,
50
- onTogglePin,
51
- searchable = false,
52
- className,
53
- }) => {
54
- const { t } = useDetailTranslation();
55
- const [newComment, setNewComment] = React.useState('');
56
- const [isSubmitting, setIsSubmitting] = React.useState(false);
57
- const [searchQuery, setSearchQuery] = React.useState('');
58
-
59
- const handleSubmit = React.useCallback(async () => {
60
- const text = newComment.trim();
61
- if (!text || !onAddComment) return;
62
-
63
- setIsSubmitting(true);
64
- try {
65
- await onAddComment(text);
66
- setNewComment('');
67
- } finally {
68
- setIsSubmitting(false);
69
- }
70
- }, [newComment, onAddComment]);
71
-
72
- const handleKeyDown = React.useCallback(
73
- (e: React.KeyboardEvent) => {
74
- if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
75
- e.preventDefault();
76
- handleSubmit();
77
- }
78
- },
79
- [handleSubmit],
80
- );
81
-
82
- /** Sort pinned comments first, then by date */
83
- const sortedComments = React.useMemo(() => {
84
- const filtered = searchQuery.trim()
85
- ? comments.filter(c => {
86
- const q = searchQuery.trim().toLowerCase();
87
- return c.text.toLowerCase().includes(q) || c.author.toLowerCase().includes(q);
88
- })
89
- : comments;
90
-
91
- return [...filtered].sort((a, b) => {
92
- if (a.pinned && !b.pinned) return -1;
93
- if (!a.pinned && b.pinned) return 1;
94
- return 0;
95
- });
96
- }, [comments, searchQuery]);
97
-
98
- return (
99
- <Card className={cn('', className)}>
100
- <CardHeader>
101
- <CardTitle className="flex items-center gap-2 text-base">
102
- <MessageSquare className="h-4 w-4" />
103
- {t('detail.comments')}
104
- <span className="text-sm font-normal text-muted-foreground">
105
- ({comments.length})
106
- </span>
107
- </CardTitle>
108
- </CardHeader>
109
- <CardContent className="space-y-4">
110
- {/* Search Input */}
111
- {searchable && (
112
- <div className="flex items-center gap-2">
113
- <div className="relative flex-1">
114
- <Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
115
- <input
116
- className="w-full rounded-md border border-input bg-background pl-8 pr-8 py-1.5 text-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
117
- placeholder={t('detail.searchComments')}
118
- value={searchQuery}
119
- onChange={(e) => setSearchQuery(e.target.value)}
120
- aria-label={t('detail.searchComments')}
121
- />
122
- {searchQuery && (
123
- <button
124
- className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
125
- onClick={() => setSearchQuery('')}
126
- aria-label={t('detail.clearSearch')}
127
- type="button"
128
- >
129
- <X className="h-3.5 w-3.5" />
130
- </button>
131
- )}
132
- </div>
133
- </div>
134
- )}
135
-
136
- {/* Comment Input */}
137
- {onAddComment && (
138
- <div className="flex gap-2">
139
- <textarea
140
- className="flex-1 min-h-[60px] rounded-md border border-input bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring resize-none"
141
- placeholder={t('detail.addCommentPlaceholder')}
142
- value={newComment}
143
- onChange={(e) => setNewComment(e.target.value)}
144
- onKeyDown={handleKeyDown}
145
- disabled={isSubmitting}
146
- />
147
- <Button
148
- size="icon"
149
- variant="default"
150
- onClick={handleSubmit}
151
- disabled={!newComment.trim() || isSubmitting}
152
- className="shrink-0 self-end"
153
- >
154
- <Send className="h-4 w-4" />
155
- </Button>
156
- </div>
157
- )}
158
-
159
- {/* Comment List */}
160
- {sortedComments.length === 0 ? (
161
- <p className="text-sm text-muted-foreground text-center py-4">
162
- {searchQuery.trim() ? t('detail.noMatchingComments') : t('detail.noCommentsYet')}
163
- </p>
164
- ) : (
165
- <div className="space-y-3">
166
- {sortedComments.map((comment) => (
167
- <div key={comment.id} className={cn('flex gap-3', comment.pinned && 'bg-muted/40 rounded-md p-2 -mx-2')}>
168
- {/* Avatar */}
169
- <div className="shrink-0">
170
- {comment.avatarUrl ? (
171
- <img
172
- src={comment.avatarUrl}
173
- alt={comment.author}
174
- className="h-8 w-8 rounded-full object-cover"
175
- />
176
- ) : (
177
- <div className="h-8 w-8 rounded-full bg-muted flex items-center justify-center text-xs font-medium text-muted-foreground">
178
- {comment.author.charAt(0).toUpperCase()}
179
- </div>
180
- )}
181
- </div>
182
- {/* Content */}
183
- <div className="flex-1 min-w-0">
184
- <div className="flex items-center gap-2 mb-0.5">
185
- <span className="text-sm font-medium truncate">{comment.author}</span>
186
- <span className="text-xs text-muted-foreground">
187
- {formatTimestamp(comment.createdAt)}
188
- </span>
189
- {comment.pinned && (
190
- <span className="text-xs text-amber-600 flex items-center gap-0.5">
191
- <Pin className="h-3 w-3" />
192
- {t('detail.pinned')}
193
- </span>
194
- )}
195
- </div>
196
- <p className="text-sm whitespace-pre-wrap break-words">{comment.text}</p>
197
- {/* Pin action */}
198
- {onTogglePin && (
199
- <button
200
- type="button"
201
- className="mt-1 text-xs text-muted-foreground hover:text-foreground flex items-center gap-1"
202
- onClick={() => onTogglePin(comment.id)}
203
- aria-label={comment.pinned ? t('detail.unpin') : t('detail.pin')}
204
- >
205
- <Pin className="h-3 w-3" />
206
- {comment.pinned ? t('detail.unpin') : t('detail.pin')}
207
- </button>
208
- )}
209
- </div>
210
- </div>
211
- ))}
212
- </div>
213
- )}
214
- </CardContent>
215
- </Card>
216
- );
217
- };
@@ -1,213 +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 { cn, Button, Input } from '@object-ui/components';
11
- import {
12
- ChevronsLeft,
13
- ChevronsRight,
14
- ChevronLeft,
15
- ChevronRight,
16
- Search,
17
- } from 'lucide-react';
18
- import { useDetailTranslation } from './useDetailTranslation';
19
-
20
- export interface RecordNavigationEnhancedProps {
21
- currentIndex: number;
22
- totalRecords: number;
23
- recordIds: string[];
24
- onNavigate: (recordId: string) => void;
25
- onSearch?: (query: string) => void;
26
- className?: string;
27
- }
28
-
29
- export const RecordNavigationEnhanced: React.FC<RecordNavigationEnhancedProps> = ({
30
- currentIndex,
31
- totalRecords,
32
- recordIds,
33
- onNavigate,
34
- onSearch,
35
- className,
36
- }) => {
37
- const { t } = useDetailTranslation();
38
- const [searchQuery, setSearchQuery] = React.useState('');
39
- const [isSearchOpen, setIsSearchOpen] = React.useState(false);
40
- const searchInputRef = React.useRef<HTMLInputElement>(null);
41
-
42
- const canGoFirst = currentIndex > 0;
43
- const canGoPrev = currentIndex > 0;
44
- const canGoNext = currentIndex < totalRecords - 1;
45
- const canGoLast = currentIndex < totalRecords - 1;
46
-
47
- const handleFirst = React.useCallback(() => {
48
- if (canGoFirst) onNavigate(recordIds[0]);
49
- }, [canGoFirst, onNavigate, recordIds]);
50
-
51
- const handlePrev = React.useCallback(() => {
52
- if (canGoPrev) onNavigate(recordIds[currentIndex - 1]);
53
- }, [canGoPrev, onNavigate, recordIds, currentIndex]);
54
-
55
- const handleNext = React.useCallback(() => {
56
- if (canGoNext) onNavigate(recordIds[currentIndex + 1]);
57
- }, [canGoNext, onNavigate, recordIds, currentIndex]);
58
-
59
- const handleLast = React.useCallback(() => {
60
- if (canGoLast) onNavigate(recordIds[recordIds.length - 1]);
61
- }, [canGoLast, onNavigate, recordIds]);
62
-
63
- const handleSearchChange = React.useCallback(
64
- (e: React.ChangeEvent<HTMLInputElement>) => {
65
- const value = e.target.value;
66
- setSearchQuery(value);
67
- onSearch?.(value);
68
- },
69
- [onSearch],
70
- );
71
-
72
- const handleToggleSearch = React.useCallback(() => {
73
- setIsSearchOpen((prev) => {
74
- if (!prev) {
75
- requestAnimationFrame(() => searchInputRef.current?.focus());
76
- } else {
77
- setSearchQuery('');
78
- onSearch?.('');
79
- }
80
- return !prev;
81
- });
82
- }, [onSearch]);
83
-
84
- // Keyboard shortcuts
85
- React.useEffect(() => {
86
- const handler = (e: KeyboardEvent) => {
87
- const tag = (e.target as HTMLElement)?.tagName;
88
- const isEditable =
89
- tag === 'INPUT' || tag === 'TEXTAREA' || (e.target as HTMLElement)?.isContentEditable;
90
-
91
- // Allow search input to capture normal keys
92
- if (isEditable && e.target !== searchInputRef.current) return;
93
- // In search input, only handle navigation keys
94
- if (e.target === searchInputRef.current) {
95
- if (e.key === 'Escape') {
96
- e.preventDefault();
97
- setIsSearchOpen(false);
98
- setSearchQuery('');
99
- onSearch?.('');
100
- (e.target as HTMLElement).blur();
101
- }
102
- return;
103
- }
104
-
105
- switch (e.key) {
106
- case 'Home':
107
- e.preventDefault();
108
- if (canGoFirst) onNavigate(recordIds[0]);
109
- break;
110
- case 'End':
111
- e.preventDefault();
112
- if (canGoLast) onNavigate(recordIds[recordIds.length - 1]);
113
- break;
114
- case 'ArrowLeft':
115
- e.preventDefault();
116
- if (canGoPrev) onNavigate(recordIds[currentIndex - 1]);
117
- break;
118
- case 'ArrowRight':
119
- e.preventDefault();
120
- if (canGoNext) onNavigate(recordIds[currentIndex + 1]);
121
- break;
122
- }
123
- };
124
-
125
- document.addEventListener('keydown', handler);
126
- return () => document.removeEventListener('keydown', handler);
127
- }, [currentIndex, recordIds, canGoFirst, canGoPrev, canGoNext, canGoLast, onNavigate, onSearch]);
128
-
129
- return (
130
- <div className={cn('flex items-center gap-1.5', className)}>
131
- {/* First */}
132
- <Button
133
- variant="outline"
134
- size="icon"
135
- className="h-8 w-8"
136
- disabled={!canGoFirst}
137
- onClick={handleFirst}
138
- title={t('detail.firstRecord')}
139
- >
140
- <ChevronsLeft className="h-4 w-4" />
141
- </Button>
142
-
143
- {/* Prev */}
144
- <Button
145
- variant="outline"
146
- size="icon"
147
- className="h-8 w-8"
148
- disabled={!canGoPrev}
149
- onClick={handlePrev}
150
- title={t('detail.previousRecordKey')}
151
- >
152
- <ChevronLeft className="h-4 w-4" />
153
- </Button>
154
-
155
- {/* Position indicator */}
156
- <span className="text-xs text-muted-foreground whitespace-nowrap px-1.5 tabular-nums">
157
- {totalRecords > 0 ? t('detail.recordOf', { current: currentIndex + 1, total: totalRecords }) : t('detail.noRecords')}
158
- </span>
159
-
160
- {/* Next */}
161
- <Button
162
- variant="outline"
163
- size="icon"
164
- className="h-8 w-8"
165
- disabled={!canGoNext}
166
- onClick={handleNext}
167
- title={t('detail.nextRecordKey')}
168
- >
169
- <ChevronRight className="h-4 w-4" />
170
- </Button>
171
-
172
- {/* Last */}
173
- <Button
174
- variant="outline"
175
- size="icon"
176
- className="h-8 w-8"
177
- disabled={!canGoLast}
178
- onClick={handleLast}
179
- title={t('detail.lastRecord')}
180
- >
181
- <ChevronsRight className="h-4 w-4" />
182
- </Button>
183
-
184
- {/* Search toggle */}
185
- {onSearch && (
186
- <>
187
- <Button
188
- variant={isSearchOpen ? 'secondary' : 'ghost'}
189
- size="icon"
190
- className="h-8 w-8"
191
- onClick={handleToggleSearch}
192
- title={t('detail.searchWhileNavigating')}
193
- >
194
- <Search className="h-4 w-4" />
195
- </Button>
196
-
197
- {isSearchOpen && (
198
- <div className="relative">
199
- <Input
200
- ref={searchInputRef}
201
- type="text"
202
- placeholder={t('detail.searchRecords')}
203
- value={searchQuery}
204
- onChange={handleSearchChange}
205
- className="h-8 w-48 text-sm"
206
- />
207
- </div>
208
- )}
209
- </>
210
- )}
211
- </div>
212
- );
213
- };