@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,215 +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
-
14
- export interface RecordCommentsProps {
15
- comments: CommentEntry[];
16
- onAddComment?: (text: string) => void | Promise<void>;
17
- /** Callback to toggle pin/star on a comment */
18
- onTogglePin?: (commentId: string | number) => void;
19
- /** Enable search input for filtering comments */
20
- searchable?: boolean;
21
- className?: string;
22
- }
23
-
24
- /**
25
- * Format a timestamp string into a human-readable relative time or date string.
26
- */
27
- function formatTimestamp(timestamp: string): string {
28
- try {
29
- const date = new Date(timestamp);
30
- const now = new Date();
31
- const diffMs = now.getTime() - date.getTime();
32
- const diffMins = Math.floor(diffMs / 60000);
33
-
34
- if (diffMins < 1) return 'just now';
35
- if (diffMins < 60) return `${diffMins}m ago`;
36
- const diffHours = Math.floor(diffMins / 60);
37
- if (diffHours < 24) return `${diffHours}h ago`;
38
- const diffDays = Math.floor(diffHours / 24);
39
- if (diffDays < 7) return `${diffDays}d ago`;
40
- return date.toLocaleDateString();
41
- } catch {
42
- return timestamp;
43
- }
44
- }
45
-
46
- export const RecordComments: React.FC<RecordCommentsProps> = ({
47
- comments,
48
- onAddComment,
49
- onTogglePin,
50
- searchable = false,
51
- className,
52
- }) => {
53
- const [newComment, setNewComment] = React.useState('');
54
- const [isSubmitting, setIsSubmitting] = React.useState(false);
55
- const [searchQuery, setSearchQuery] = React.useState('');
56
-
57
- const handleSubmit = React.useCallback(async () => {
58
- const text = newComment.trim();
59
- if (!text || !onAddComment) return;
60
-
61
- setIsSubmitting(true);
62
- try {
63
- await onAddComment(text);
64
- setNewComment('');
65
- } finally {
66
- setIsSubmitting(false);
67
- }
68
- }, [newComment, onAddComment]);
69
-
70
- const handleKeyDown = React.useCallback(
71
- (e: React.KeyboardEvent) => {
72
- if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
73
- e.preventDefault();
74
- handleSubmit();
75
- }
76
- },
77
- [handleSubmit],
78
- );
79
-
80
- /** Sort pinned comments first, then by date */
81
- const sortedComments = React.useMemo(() => {
82
- const filtered = searchQuery.trim()
83
- ? comments.filter(c => {
84
- const q = searchQuery.trim().toLowerCase();
85
- return c.text.toLowerCase().includes(q) || c.author.toLowerCase().includes(q);
86
- })
87
- : comments;
88
-
89
- return [...filtered].sort((a, b) => {
90
- if (a.pinned && !b.pinned) return -1;
91
- if (!a.pinned && b.pinned) return 1;
92
- return 0;
93
- });
94
- }, [comments, searchQuery]);
95
-
96
- return (
97
- <Card className={cn('', className)}>
98
- <CardHeader>
99
- <CardTitle className="flex items-center gap-2 text-base">
100
- <MessageSquare className="h-4 w-4" />
101
- Comments
102
- <span className="text-sm font-normal text-muted-foreground">
103
- ({comments.length})
104
- </span>
105
- </CardTitle>
106
- </CardHeader>
107
- <CardContent className="space-y-4">
108
- {/* Search Input */}
109
- {searchable && (
110
- <div className="flex items-center gap-2">
111
- <div className="relative flex-1">
112
- <Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
113
- <input
114
- 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"
115
- placeholder="Search comments…"
116
- value={searchQuery}
117
- onChange={(e) => setSearchQuery(e.target.value)}
118
- aria-label="Search comments"
119
- />
120
- {searchQuery && (
121
- <button
122
- className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
123
- onClick={() => setSearchQuery('')}
124
- aria-label="Clear search"
125
- type="button"
126
- >
127
- <X className="h-3.5 w-3.5" />
128
- </button>
129
- )}
130
- </div>
131
- </div>
132
- )}
133
-
134
- {/* Comment Input */}
135
- {onAddComment && (
136
- <div className="flex gap-2">
137
- <textarea
138
- 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"
139
- placeholder="Add a comment… (Ctrl+Enter to submit)"
140
- value={newComment}
141
- onChange={(e) => setNewComment(e.target.value)}
142
- onKeyDown={handleKeyDown}
143
- disabled={isSubmitting}
144
- />
145
- <Button
146
- size="icon"
147
- variant="default"
148
- onClick={handleSubmit}
149
- disabled={!newComment.trim() || isSubmitting}
150
- className="shrink-0 self-end"
151
- >
152
- <Send className="h-4 w-4" />
153
- </Button>
154
- </div>
155
- )}
156
-
157
- {/* Comment List */}
158
- {sortedComments.length === 0 ? (
159
- <p className="text-sm text-muted-foreground text-center py-4">
160
- {searchQuery.trim() ? 'No matching comments' : 'No comments yet'}
161
- </p>
162
- ) : (
163
- <div className="space-y-3">
164
- {sortedComments.map((comment) => (
165
- <div key={comment.id} className={cn('flex gap-3', comment.pinned && 'bg-muted/40 rounded-md p-2 -mx-2')}>
166
- {/* Avatar */}
167
- <div className="shrink-0">
168
- {comment.avatarUrl ? (
169
- <img
170
- src={comment.avatarUrl}
171
- alt={comment.author}
172
- className="h-8 w-8 rounded-full object-cover"
173
- />
174
- ) : (
175
- <div className="h-8 w-8 rounded-full bg-muted flex items-center justify-center text-xs font-medium text-muted-foreground">
176
- {comment.author.charAt(0).toUpperCase()}
177
- </div>
178
- )}
179
- </div>
180
- {/* Content */}
181
- <div className="flex-1 min-w-0">
182
- <div className="flex items-center gap-2 mb-0.5">
183
- <span className="text-sm font-medium truncate">{comment.author}</span>
184
- <span className="text-xs text-muted-foreground">
185
- {formatTimestamp(comment.createdAt)}
186
- </span>
187
- {comment.pinned && (
188
- <span className="text-xs text-amber-600 flex items-center gap-0.5">
189
- <Pin className="h-3 w-3" />
190
- Pinned
191
- </span>
192
- )}
193
- </div>
194
- <p className="text-sm whitespace-pre-wrap break-words">{comment.text}</p>
195
- {/* Pin action */}
196
- {onTogglePin && (
197
- <button
198
- type="button"
199
- className="mt-1 text-xs text-muted-foreground hover:text-foreground flex items-center gap-1"
200
- onClick={() => onTogglePin(comment.id)}
201
- aria-label={comment.pinned ? 'Unpin comment' : 'Pin comment'}
202
- >
203
- <Pin className="h-3 w-3" />
204
- {comment.pinned ? 'Unpin' : 'Pin'}
205
- </button>
206
- )}
207
- </div>
208
- </div>
209
- ))}
210
- </div>
211
- )}
212
- </CardContent>
213
- </Card>
214
- );
215
- };
@@ -1,211 +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
-
19
- export interface RecordNavigationEnhancedProps {
20
- currentIndex: number;
21
- totalRecords: number;
22
- recordIds: string[];
23
- onNavigate: (recordId: string) => void;
24
- onSearch?: (query: string) => void;
25
- className?: string;
26
- }
27
-
28
- export const RecordNavigationEnhanced: React.FC<RecordNavigationEnhancedProps> = ({
29
- currentIndex,
30
- totalRecords,
31
- recordIds,
32
- onNavigate,
33
- onSearch,
34
- className,
35
- }) => {
36
- const [searchQuery, setSearchQuery] = React.useState('');
37
- const [isSearchOpen, setIsSearchOpen] = React.useState(false);
38
- const searchInputRef = React.useRef<HTMLInputElement>(null);
39
-
40
- const canGoFirst = currentIndex > 0;
41
- const canGoPrev = currentIndex > 0;
42
- const canGoNext = currentIndex < totalRecords - 1;
43
- const canGoLast = currentIndex < totalRecords - 1;
44
-
45
- const handleFirst = React.useCallback(() => {
46
- if (canGoFirst) onNavigate(recordIds[0]);
47
- }, [canGoFirst, onNavigate, recordIds]);
48
-
49
- const handlePrev = React.useCallback(() => {
50
- if (canGoPrev) onNavigate(recordIds[currentIndex - 1]);
51
- }, [canGoPrev, onNavigate, recordIds, currentIndex]);
52
-
53
- const handleNext = React.useCallback(() => {
54
- if (canGoNext) onNavigate(recordIds[currentIndex + 1]);
55
- }, [canGoNext, onNavigate, recordIds, currentIndex]);
56
-
57
- const handleLast = React.useCallback(() => {
58
- if (canGoLast) onNavigate(recordIds[recordIds.length - 1]);
59
- }, [canGoLast, onNavigate, recordIds]);
60
-
61
- const handleSearchChange = React.useCallback(
62
- (e: React.ChangeEvent<HTMLInputElement>) => {
63
- const value = e.target.value;
64
- setSearchQuery(value);
65
- onSearch?.(value);
66
- },
67
- [onSearch],
68
- );
69
-
70
- const handleToggleSearch = React.useCallback(() => {
71
- setIsSearchOpen((prev) => {
72
- if (!prev) {
73
- requestAnimationFrame(() => searchInputRef.current?.focus());
74
- } else {
75
- setSearchQuery('');
76
- onSearch?.('');
77
- }
78
- return !prev;
79
- });
80
- }, [onSearch]);
81
-
82
- // Keyboard shortcuts
83
- React.useEffect(() => {
84
- const handler = (e: KeyboardEvent) => {
85
- const tag = (e.target as HTMLElement)?.tagName;
86
- const isEditable =
87
- tag === 'INPUT' || tag === 'TEXTAREA' || (e.target as HTMLElement)?.isContentEditable;
88
-
89
- // Allow search input to capture normal keys
90
- if (isEditable && e.target !== searchInputRef.current) return;
91
- // In search input, only handle navigation keys
92
- if (e.target === searchInputRef.current) {
93
- if (e.key === 'Escape') {
94
- e.preventDefault();
95
- setIsSearchOpen(false);
96
- setSearchQuery('');
97
- onSearch?.('');
98
- (e.target as HTMLElement).blur();
99
- }
100
- return;
101
- }
102
-
103
- switch (e.key) {
104
- case 'Home':
105
- e.preventDefault();
106
- if (canGoFirst) onNavigate(recordIds[0]);
107
- break;
108
- case 'End':
109
- e.preventDefault();
110
- if (canGoLast) onNavigate(recordIds[recordIds.length - 1]);
111
- break;
112
- case 'ArrowLeft':
113
- e.preventDefault();
114
- if (canGoPrev) onNavigate(recordIds[currentIndex - 1]);
115
- break;
116
- case 'ArrowRight':
117
- e.preventDefault();
118
- if (canGoNext) onNavigate(recordIds[currentIndex + 1]);
119
- break;
120
- }
121
- };
122
-
123
- document.addEventListener('keydown', handler);
124
- return () => document.removeEventListener('keydown', handler);
125
- }, [currentIndex, recordIds, canGoFirst, canGoPrev, canGoNext, canGoLast, onNavigate, onSearch]);
126
-
127
- return (
128
- <div className={cn('flex items-center gap-1.5', className)}>
129
- {/* First */}
130
- <Button
131
- variant="outline"
132
- size="icon"
133
- className="h-8 w-8"
134
- disabled={!canGoFirst}
135
- onClick={handleFirst}
136
- title="First record (Home)"
137
- >
138
- <ChevronsLeft className="h-4 w-4" />
139
- </Button>
140
-
141
- {/* Prev */}
142
- <Button
143
- variant="outline"
144
- size="icon"
145
- className="h-8 w-8"
146
- disabled={!canGoPrev}
147
- onClick={handlePrev}
148
- title="Previous record (←)"
149
- >
150
- <ChevronLeft className="h-4 w-4" />
151
- </Button>
152
-
153
- {/* Position indicator */}
154
- <span className="text-xs text-muted-foreground whitespace-nowrap px-1.5 tabular-nums">
155
- {totalRecords > 0 ? `${currentIndex + 1} of ${totalRecords}` : 'No records'}
156
- </span>
157
-
158
- {/* Next */}
159
- <Button
160
- variant="outline"
161
- size="icon"
162
- className="h-8 w-8"
163
- disabled={!canGoNext}
164
- onClick={handleNext}
165
- title="Next record (→)"
166
- >
167
- <ChevronRight className="h-4 w-4" />
168
- </Button>
169
-
170
- {/* Last */}
171
- <Button
172
- variant="outline"
173
- size="icon"
174
- className="h-8 w-8"
175
- disabled={!canGoLast}
176
- onClick={handleLast}
177
- title="Last record (End)"
178
- >
179
- <ChevronsRight className="h-4 w-4" />
180
- </Button>
181
-
182
- {/* Search toggle */}
183
- {onSearch && (
184
- <>
185
- <Button
186
- variant={isSearchOpen ? 'secondary' : 'ghost'}
187
- size="icon"
188
- className="h-8 w-8"
189
- onClick={handleToggleSearch}
190
- title="Search while navigating"
191
- >
192
- <Search className="h-4 w-4" />
193
- </Button>
194
-
195
- {isSearchOpen && (
196
- <div className="relative">
197
- <Input
198
- ref={searchInputRef}
199
- type="text"
200
- placeholder="Search records…"
201
- value={searchQuery}
202
- onChange={handleSearchChange}
203
- className="h-8 w-48 text-sm"
204
- />
205
- </div>
206
- )}
207
- </>
208
- )}
209
- </div>
210
- );
211
- };