@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,350 +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 } from '@object-ui/components';
11
- import {
12
- Bold,
13
- Italic,
14
- List,
15
- Code,
16
- AtSign,
17
- Eye,
18
- Edit,
19
- Send,
20
- } from 'lucide-react';
21
- import { useDetailTranslation } from './useDetailTranslation';
22
-
23
- export interface MentionSuggestion {
24
- id: string;
25
- label: string;
26
- avatarUrl?: string;
27
- }
28
-
29
- export interface RichTextCommentInputProps {
30
- value: string;
31
- onChange: (value: string) => void;
32
- onSubmit?: () => void | Promise<void>;
33
- mentionSuggestions?: MentionSuggestion[];
34
- placeholder?: string;
35
- className?: string;
36
- disabled?: boolean;
37
- }
38
-
39
- /** Render minimal markdown to HTML for preview. */
40
- function renderMarkdown(text: string): string {
41
- let html = text
42
- // Escape HTML
43
- .replace(/&/g, '&amp;')
44
- .replace(/</g, '&lt;')
45
- .replace(/>/g, '&gt;')
46
- // Code blocks (```)
47
- .replace(/```([\s\S]*?)```/g, '<pre class="bg-muted rounded p-2 text-xs font-mono my-1 overflow-x-auto">$1</pre>')
48
- // Inline code
49
- .replace(/`([^`]+)`/g, '<code class="bg-muted rounded px-1 py-0.5 text-xs font-mono">$1</code>')
50
- // Bold
51
- .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
52
- // Italic
53
- .replace(/\*(.+?)\*/g, '<em>$1</em>')
54
- // @mentions
55
- .replace(/@(\w+)/g, '<span class="text-primary font-medium">@$1</span>')
56
- // Unordered lists
57
- .replace(/^- (.+)$/gm, '<li class="ml-4 list-disc">$1</li>')
58
- // Line breaks
59
- .replace(/\n/g, '<br/>');
60
-
61
- // Wrap consecutive <li> in <ul>
62
- html = html.replace(
63
- /(<li[^>]*>.*?<\/li>(?:<br\/>)?)+/g,
64
- (match) => `<ul class="my-1">${match.replace(/<br\/>/g, '')}</ul>`,
65
- );
66
-
67
- return html;
68
- }
69
-
70
- export const RichTextCommentInput: React.FC<RichTextCommentInputProps> = ({
71
- value,
72
- onChange,
73
- onSubmit,
74
- mentionSuggestions = [],
75
- placeholder,
76
- className,
77
- disabled = false,
78
- }) => {
79
- const { t } = useDetailTranslation();
80
- const [isPreview, setIsPreview] = React.useState(false);
81
- const [showMentions, setShowMentions] = React.useState(false);
82
- const [mentionQuery, setMentionQuery] = React.useState('');
83
- const [mentionIndex, setMentionIndex] = React.useState(0);
84
- const textareaRef = React.useRef<HTMLTextAreaElement>(null);
85
-
86
- const filteredMentions = React.useMemo(() => {
87
- if (!mentionQuery) return mentionSuggestions;
88
- const query = mentionQuery.toLowerCase();
89
- return mentionSuggestions.filter((s) =>
90
- s.label.toLowerCase().includes(query),
91
- );
92
- }, [mentionQuery, mentionSuggestions]);
93
-
94
- const insertAtCursor = React.useCallback(
95
- (before: string, after: string = '') => {
96
- const textarea = textareaRef.current;
97
- if (!textarea) return;
98
- const start = textarea.selectionStart;
99
- const end = textarea.selectionEnd;
100
- const selected = value.slice(start, end);
101
- const newValue =
102
- value.slice(0, start) + before + selected + after + value.slice(end);
103
- onChange(newValue);
104
- // Restore cursor after the insertion
105
- requestAnimationFrame(() => {
106
- textarea.focus();
107
- const cursorPos = start + before.length + selected.length;
108
- textarea.setSelectionRange(cursorPos, cursorPos);
109
- });
110
- },
111
- [value, onChange],
112
- );
113
-
114
- const handleBold = React.useCallback(() => insertAtCursor('**', '**'), [insertAtCursor]);
115
- const handleItalic = React.useCallback(() => insertAtCursor('*', '*'), [insertAtCursor]);
116
- const handleList = React.useCallback(() => insertAtCursor('\n- ', ''), [insertAtCursor]);
117
- const handleCode = React.useCallback(() => insertAtCursor('`', '`'), [insertAtCursor]);
118
-
119
- const handleMentionTrigger = React.useCallback(() => {
120
- insertAtCursor('@', '');
121
- setShowMentions(true);
122
- setMentionQuery('');
123
- setMentionIndex(0);
124
- }, [insertAtCursor]);
125
-
126
- const handleSelectMention = React.useCallback(
127
- (suggestion: MentionSuggestion) => {
128
- const textarea = textareaRef.current;
129
- if (!textarea) return;
130
- const cursorPos = textarea.selectionStart;
131
- // Find the last '@' before cursor to replace the partial query
132
- const textBefore = value.slice(0, cursorPos);
133
- const atIndex = textBefore.lastIndexOf('@');
134
- if (atIndex !== -1) {
135
- const newValue =
136
- value.slice(0, atIndex) + `@${suggestion.label} ` + value.slice(cursorPos);
137
- onChange(newValue);
138
- }
139
- setShowMentions(false);
140
- setMentionQuery('');
141
- requestAnimationFrame(() => textarea.focus());
142
- },
143
- [value, onChange],
144
- );
145
-
146
- const handleTextChange = React.useCallback(
147
- (e: React.ChangeEvent<HTMLTextAreaElement>) => {
148
- const newValue = e.target.value;
149
- onChange(newValue);
150
-
151
- // Detect @mention trigger
152
- const cursorPos = e.target.selectionStart;
153
- const textBefore = newValue.slice(0, cursorPos);
154
- const lastAtIndex = textBefore.lastIndexOf('@');
155
-
156
- if (lastAtIndex !== -1) {
157
- const textAfterAt = textBefore.slice(lastAtIndex + 1);
158
- // Only show if @ is at start or preceded by whitespace and no space in query
159
- const charBeforeAt = lastAtIndex > 0 ? textBefore[lastAtIndex - 1] : ' ';
160
- if (/\s/.test(charBeforeAt) && !/\s/.test(textAfterAt)) {
161
- setShowMentions(true);
162
- setMentionQuery(textAfterAt);
163
- setMentionIndex(0);
164
- return;
165
- }
166
- }
167
- setShowMentions(false);
168
- },
169
- [onChange],
170
- );
171
-
172
- const handleKeyDown = React.useCallback(
173
- (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
174
- if (showMentions && filteredMentions.length > 0) {
175
- if (e.key === 'ArrowDown') {
176
- e.preventDefault();
177
- setMentionIndex((prev) =>
178
- prev < filteredMentions.length - 1 ? prev + 1 : 0,
179
- );
180
- return;
181
- }
182
- if (e.key === 'ArrowUp') {
183
- e.preventDefault();
184
- setMentionIndex((prev) =>
185
- prev > 0 ? prev - 1 : filteredMentions.length - 1,
186
- );
187
- return;
188
- }
189
- if (e.key === 'Enter' || e.key === 'Tab') {
190
- e.preventDefault();
191
- handleSelectMention(filteredMentions[mentionIndex]);
192
- return;
193
- }
194
- if (e.key === 'Escape') {
195
- setShowMentions(false);
196
- return;
197
- }
198
- }
199
-
200
- // Ctrl+Enter to submit
201
- if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
202
- e.preventDefault();
203
- onSubmit?.();
204
- }
205
- },
206
- [showMentions, filteredMentions, mentionIndex, handleSelectMention, onSubmit],
207
- );
208
-
209
- return (
210
- <div className={cn('border rounded-md', className)}>
211
- {/* Toolbar */}
212
- <div className="flex items-center gap-0.5 px-2 py-1.5 border-b bg-muted/30">
213
- <Button
214
- variant="ghost"
215
- size="icon"
216
- className="h-7 w-7"
217
- onClick={handleBold}
218
- disabled={disabled || isPreview}
219
- title={t('detail.bold')}
220
- >
221
- <Bold className="h-3.5 w-3.5" />
222
- </Button>
223
- <Button
224
- variant="ghost"
225
- size="icon"
226
- className="h-7 w-7"
227
- onClick={handleItalic}
228
- disabled={disabled || isPreview}
229
- title={t('detail.italic')}
230
- >
231
- <Italic className="h-3.5 w-3.5" />
232
- </Button>
233
- <Button
234
- variant="ghost"
235
- size="icon"
236
- className="h-7 w-7"
237
- onClick={handleList}
238
- disabled={disabled || isPreview}
239
- title={t('detail.listFormat')}
240
- >
241
- <List className="h-3.5 w-3.5" />
242
- </Button>
243
- <Button
244
- variant="ghost"
245
- size="icon"
246
- className="h-7 w-7"
247
- onClick={handleCode}
248
- disabled={disabled || isPreview}
249
- title={t('detail.inlineCode')}
250
- >
251
- <Code className="h-3.5 w-3.5" />
252
- </Button>
253
- <Button
254
- variant="ghost"
255
- size="icon"
256
- className="h-7 w-7"
257
- onClick={handleMentionTrigger}
258
- disabled={disabled || isPreview}
259
- title={t('detail.mentionSomeone')}
260
- >
261
- <AtSign className="h-3.5 w-3.5" />
262
- </Button>
263
-
264
- <div className="flex-1" />
265
-
266
- <Button
267
- variant="ghost"
268
- size="icon"
269
- className="h-7 w-7"
270
- onClick={() => setIsPreview(!isPreview)}
271
- title={isPreview ? t('detail.edit') : t('detail.preview')}
272
- >
273
- {isPreview ? (
274
- <Edit className="h-3.5 w-3.5" />
275
- ) : (
276
- <Eye className="h-3.5 w-3.5" />
277
- )}
278
- </Button>
279
-
280
- {onSubmit && (
281
- <Button
282
- variant="ghost"
283
- size="icon"
284
- className="h-7 w-7"
285
- onClick={onSubmit}
286
- disabled={disabled || !value.trim()}
287
- title={t('detail.submitComment')}
288
- >
289
- <Send className="h-3.5 w-3.5" />
290
- </Button>
291
- )}
292
- </div>
293
-
294
- {/* Editor / Preview */}
295
- <div className="relative">
296
- {isPreview ? (
297
- <div
298
- className="min-h-[80px] px-3 py-2 text-sm prose prose-sm max-w-none"
299
- dangerouslySetInnerHTML={{ __html: renderMarkdown(value) }}
300
- />
301
- ) : (
302
- <>
303
- <textarea
304
- ref={textareaRef}
305
- className="w-full min-h-[80px] px-3 py-2 text-sm bg-transparent resize-none focus:outline-none placeholder:text-muted-foreground"
306
- placeholder={placeholder ?? t('detail.writeComment')}
307
- value={value}
308
- onChange={handleTextChange}
309
- onKeyDown={handleKeyDown}
310
- disabled={disabled}
311
- />
312
-
313
- {/* @mention dropdown */}
314
- {showMentions && filteredMentions.length > 0 && (
315
- <div className="absolute left-2 bottom-full mb-1 w-56 bg-popover border rounded-md shadow-md z-50 max-h-40 overflow-y-auto">
316
- {filteredMentions.map((suggestion, index) => (
317
- <button
318
- key={suggestion.id}
319
- type="button"
320
- className={cn(
321
- 'w-full text-left px-3 py-1.5 text-sm flex items-center gap-2 hover:bg-accent transition-colors',
322
- index === mentionIndex && 'bg-accent',
323
- )}
324
- onMouseDown={(e) => {
325
- e.preventDefault();
326
- handleSelectMention(suggestion);
327
- }}
328
- >
329
- {suggestion.avatarUrl ? (
330
- <img
331
- src={suggestion.avatarUrl}
332
- alt={suggestion.label}
333
- className="h-5 w-5 rounded-full object-cover"
334
- />
335
- ) : (
336
- <div className="h-5 w-5 rounded-full bg-muted flex items-center justify-center text-[10px] font-medium">
337
- {suggestion.label.charAt(0).toUpperCase()}
338
- </div>
339
- )}
340
- <span>{suggestion.label}</span>
341
- </button>
342
- ))}
343
- </div>
344
- )}
345
- </>
346
- )}
347
- </div>
348
- </div>
349
- );
350
- };
@@ -1,101 +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 {
11
- cn,
12
- Collapsible,
13
- CollapsibleTrigger,
14
- CollapsibleContent,
15
- } from '@object-ui/components';
16
- import { ChevronDown, ChevronRight } from 'lucide-react';
17
- import { DetailSection } from './DetailSection';
18
- import type { SectionGroup as SectionGroupType } from '@object-ui/types';
19
-
20
- export interface SectionGroupProps {
21
- group: SectionGroupType;
22
- data?: any;
23
- className?: string;
24
- objectSchema?: any;
25
- /** Object name for i18n field label resolution */
26
- objectName?: string;
27
- isEditing?: boolean;
28
- onFieldChange?: (field: string, value: any) => void;
29
- }
30
-
31
- export const SectionGroup: React.FC<SectionGroupProps> = ({
32
- group,
33
- data,
34
- className,
35
- objectSchema,
36
- objectName,
37
- isEditing = false,
38
- onFieldChange,
39
- }) => {
40
- const collapsible = group.collapsible ?? true;
41
- const [isCollapsed, setIsCollapsed] = React.useState(group.defaultCollapsed ?? false);
42
-
43
- const sectionsContent = (
44
- <div className="space-y-3 sm:space-y-4">
45
- {group.sections.map((section, index) => (
46
- <DetailSection
47
- key={index}
48
- section={section}
49
- data={data}
50
- objectSchema={objectSchema}
51
- objectName={objectName}
52
- isEditing={isEditing}
53
- onFieldChange={onFieldChange}
54
- />
55
- ))}
56
- </div>
57
- );
58
-
59
- if (!collapsible) {
60
- return (
61
- <div className={cn('space-y-3', className)}>
62
- <div className="flex items-center gap-2 pb-2 border-b">
63
- {group.icon && <span className="text-muted-foreground">{group.icon}</span>}
64
- <h3 className="text-lg font-semibold">{group.title}</h3>
65
- </div>
66
- {group.description && (
67
- <p className="text-sm text-muted-foreground">{group.description}</p>
68
- )}
69
- {sectionsContent}
70
- </div>
71
- );
72
- }
73
-
74
- return (
75
- <Collapsible
76
- open={!isCollapsed}
77
- onOpenChange={(open) => setIsCollapsed(!open)}
78
- className={className}
79
- >
80
- <CollapsibleTrigger asChild>
81
- <div className="flex items-center gap-2 pb-2 border-b cursor-pointer hover:bg-muted/50 transition-colors rounded-t-md px-2 py-1.5">
82
- {isCollapsed ? (
83
- <ChevronRight className="h-4 w-4 text-muted-foreground shrink-0" />
84
- ) : (
85
- <ChevronDown className="h-4 w-4 text-muted-foreground shrink-0" />
86
- )}
87
- {group.icon && <span className="text-muted-foreground">{group.icon}</span>}
88
- <h3 className="text-lg font-semibold">{group.title}</h3>
89
- </div>
90
- </CollapsibleTrigger>
91
- {group.description && !isCollapsed && (
92
- <p className="text-sm text-muted-foreground mt-1">{group.description}</p>
93
- )}
94
- <CollapsibleContent>
95
- <div className="mt-3">
96
- {sectionsContent}
97
- </div>
98
- </CollapsibleContent>
99
- </Collapsible>
100
- );
101
- };
@@ -1,62 +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 } from '@object-ui/components';
11
- import { Bell, BellOff } from 'lucide-react';
12
- import type { RecordSubscription } from '@object-ui/types';
13
- import { useDetailTranslation } from './useDetailTranslation';
14
-
15
- export interface SubscriptionToggleProps {
16
- /** Current subscription state */
17
- subscription: RecordSubscription;
18
- /** Called when user toggles subscription */
19
- onToggle?: (subscribed: boolean) => void | Promise<void>;
20
- className?: string;
21
- }
22
-
23
- /**
24
- * SubscriptionToggle — Bell icon toggle for record notification subscriptions.
25
- * Aligned with @objectstack/spec RecordSubscriptionSchema.
26
- */
27
- export const SubscriptionToggle: React.FC<SubscriptionToggleProps> = ({
28
- subscription,
29
- onToggle,
30
- className,
31
- }) => {
32
- const { t } = useDetailTranslation();
33
- const [isLoading, setIsLoading] = React.useState(false);
34
-
35
- const handleToggle = React.useCallback(async () => {
36
- if (!onToggle) return;
37
- setIsLoading(true);
38
- try {
39
- await onToggle(!subscription.subscribed);
40
- } finally {
41
- setIsLoading(false);
42
- }
43
- }, [onToggle, subscription.subscribed]);
44
-
45
- return (
46
- <Button
47
- variant="ghost"
48
- size="icon"
49
- className={cn('h-8 w-8', className)}
50
- onClick={handleToggle}
51
- disabled={isLoading || !onToggle}
52
- aria-label={subscription.subscribed ? t('detail.unsubscribeAriaLabel') : t('detail.subscribeAriaLabel')}
53
- title={subscription.subscribed ? t('detail.subscribedTooltip') : t('detail.unsubscribedTooltip')}
54
- >
55
- {subscription.subscribed ? (
56
- <Bell className="h-4 w-4 text-primary" />
57
- ) : (
58
- <BellOff className="h-4 w-4 text-muted-foreground" />
59
- )}
60
- </Button>
61
- );
62
- };
@@ -1,163 +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 } from '@object-ui/components';
11
- import { MessageSquare, ChevronDown, ChevronRight, Send } from 'lucide-react';
12
- import type { FeedItem } from '@object-ui/types';
13
- import { useDetailTranslation } from './useDetailTranslation';
14
-
15
- export interface ThreadedRepliesProps {
16
- /** Parent feed item (root comment) */
17
- parentItem: FeedItem;
18
- /** Reply feed items (children) */
19
- replies: FeedItem[];
20
- /** Called when a reply is submitted */
21
- onAddReply?: (parentId: string | number, text: string) => void | Promise<void>;
22
- /** Whether to show the reply input */
23
- showReplyInput?: boolean;
24
- className?: string;
25
- }
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
- /**
47
- * ThreadedReplies — Displays a collapsible thread of replies under a parent comment.
48
- * Supports adding new replies and showing reply count.
49
- */
50
- export const ThreadedReplies: React.FC<ThreadedRepliesProps> = ({
51
- parentItem,
52
- replies,
53
- onAddReply,
54
- showReplyInput = true,
55
- className,
56
- }) => {
57
- const { t } = useDetailTranslation();
58
- const [expanded, setExpanded] = React.useState(false);
59
- const [replyText, setReplyText] = React.useState('');
60
- const [isSubmitting, setIsSubmitting] = React.useState(false);
61
-
62
- const handleSubmitReply = React.useCallback(async () => {
63
- const text = replyText.trim();
64
- if (!text || !onAddReply) return;
65
- setIsSubmitting(true);
66
- try {
67
- await onAddReply(parentItem.id, text);
68
- setReplyText('');
69
- } finally {
70
- setIsSubmitting(false);
71
- }
72
- }, [replyText, onAddReply, parentItem.id]);
73
-
74
- const handleKeyDown = React.useCallback(
75
- (e: React.KeyboardEvent) => {
76
- if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
77
- e.preventDefault();
78
- handleSubmitReply();
79
- }
80
- },
81
- [handleSubmitReply],
82
- );
83
-
84
- if (replies.length === 0 && !showReplyInput) return null;
85
-
86
- return (
87
- <div className={cn('ml-10 mt-1', className)}>
88
- {/* Toggle */}
89
- {replies.length > 0 && (
90
- <button
91
- type="button"
92
- className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors mb-1"
93
- onClick={() => setExpanded(!expanded)}
94
- aria-expanded={expanded}
95
- >
96
- {expanded ? (
97
- <ChevronDown className="h-3 w-3" />
98
- ) : (
99
- <ChevronRight className="h-3 w-3" />
100
- )}
101
- <MessageSquare className="h-3 w-3" />
102
- <span>{replies.length === 1 ? t('detail.replyCount', { count: replies.length }) : t('detail.replyCountPlural', { count: replies.length })}</span>
103
- </button>
104
- )}
105
-
106
- {/* Replies list */}
107
- {expanded && (
108
- <div className="space-y-2 border-l-2 border-border pl-3">
109
- {replies.map((reply) => (
110
- <div key={reply.id} className="flex gap-2">
111
- <div className="shrink-0">
112
- {reply.actorAvatarUrl ? (
113
- <img
114
- src={reply.actorAvatarUrl}
115
- alt={reply.actor}
116
- className="h-6 w-6 rounded-full object-cover"
117
- />
118
- ) : (
119
- <div className="h-6 w-6 rounded-full bg-muted flex items-center justify-center text-[10px] font-medium text-muted-foreground">
120
- {reply.actor.charAt(0).toUpperCase()}
121
- </div>
122
- )}
123
- </div>
124
- <div className="flex-1 min-w-0">
125
- <div className="flex items-center gap-1.5">
126
- <span className="text-xs font-medium">{reply.actor}</span>
127
- <span className="text-[10px] text-muted-foreground">
128
- {formatTimestamp(reply.createdAt)}
129
- </span>
130
- </div>
131
- <p className="text-xs whitespace-pre-wrap break-words">{reply.body}</p>
132
- </div>
133
- </div>
134
- ))}
135
- </div>
136
- )}
137
-
138
- {/* Reply input */}
139
- {showReplyInput && onAddReply && (
140
- <div className="flex gap-1.5 mt-1.5">
141
- <input
142
- className="flex-1 rounded-md border border-input bg-background px-2 py-1 text-xs placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
143
- placeholder={t('detail.replyPlaceholder')}
144
- value={replyText}
145
- onChange={(e) => setReplyText(e.target.value)}
146
- onKeyDown={handleKeyDown}
147
- disabled={isSubmitting}
148
- />
149
- <Button
150
- variant="ghost"
151
- size="icon"
152
- className="h-6 w-6 shrink-0"
153
- onClick={handleSubmitReply}
154
- disabled={!replyText.trim() || isSubmitting}
155
- aria-label="Send reply"
156
- >
157
- <Send className="h-3 w-3" />
158
- </Button>
159
- </div>
160
- )}
161
- </div>
162
- );
163
- };