@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,123 +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 } from '@object-ui/components';
11
- import type { Mention } from '@object-ui/types';
12
-
13
- export interface MentionSuggestionItem {
14
- /** Entity ID */
15
- id: string;
16
- /** Display name */
17
- name: string;
18
- /** Avatar URL */
19
- avatarUrl?: string;
20
- /** Entity type */
21
- type: 'user' | 'team' | 'group';
22
- }
23
-
24
- export interface MentionAutocompleteProps {
25
- /** Search query (text after @) */
26
- query: string;
27
- /** Available suggestions */
28
- suggestions: MentionSuggestionItem[];
29
- /** Called when a suggestion is selected */
30
- onSelect: (item: MentionSuggestionItem) => void;
31
- /** Whether the dropdown is visible */
32
- visible?: boolean;
33
- /** Active/highlighted index */
34
- activeIndex?: number;
35
- className?: string;
36
- }
37
-
38
- /**
39
- * MentionAutocomplete — Dropdown for @mention autocomplete.
40
- * Filters suggestions by query and renders a selectable list.
41
- * Produces MentionSchema data on selection.
42
- */
43
- export const MentionAutocomplete: React.FC<MentionAutocompleteProps> = ({
44
- query,
45
- suggestions,
46
- onSelect,
47
- visible = true,
48
- activeIndex = 0,
49
- className,
50
- }) => {
51
- const filtered = React.useMemo(() => {
52
- if (!query) return suggestions;
53
- const q = query.toLowerCase();
54
- return suggestions.filter(
55
- (s) => s.name.toLowerCase().includes(q) || s.id.toLowerCase().includes(q),
56
- );
57
- }, [query, suggestions]);
58
-
59
- if (!visible || filtered.length === 0) return null;
60
-
61
- return (
62
- <div
63
- className={cn(
64
- 'bg-popover border rounded-md shadow-md z-50 max-h-48 overflow-y-auto w-56',
65
- className,
66
- )}
67
- role="listbox"
68
- aria-label="Mention suggestions"
69
- >
70
- {filtered.map((item, index) => (
71
- <button
72
- key={item.id}
73
- type="button"
74
- role="option"
75
- aria-selected={index === activeIndex}
76
- className={cn(
77
- 'w-full text-left px-3 py-1.5 text-sm flex items-center gap-2 hover:bg-accent transition-colors',
78
- index === activeIndex && 'bg-accent',
79
- )}
80
- onMouseDown={(e) => {
81
- e.preventDefault();
82
- onSelect(item);
83
- }}
84
- >
85
- {item.avatarUrl ? (
86
- <img
87
- src={item.avatarUrl}
88
- alt={item.name}
89
- className="h-5 w-5 rounded-full object-cover"
90
- />
91
- ) : (
92
- <div className="h-5 w-5 rounded-full bg-muted flex items-center justify-center text-[10px] font-medium">
93
- {item.name.charAt(0).toUpperCase()}
94
- </div>
95
- )}
96
- <div className="flex-1 min-w-0">
97
- <span className="truncate">{item.name}</span>
98
- {item.type !== 'user' && (
99
- <span className="ml-1 text-xs text-muted-foreground capitalize">({item.type})</span>
100
- )}
101
- </div>
102
- </button>
103
- ))}
104
- </div>
105
- );
106
- };
107
-
108
- /**
109
- * Helper to create a Mention object from a suggestion item.
110
- */
111
- export function createMentionFromSuggestion(
112
- item: MentionSuggestionItem,
113
- offset: number,
114
- length: number,
115
- ): Mention {
116
- return {
117
- type: item.type,
118
- id: item.id,
119
- name: item.name,
120
- offset,
121
- length,
122
- };
123
- }
@@ -1,261 +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
- Button,
13
- Card,
14
- CardHeader,
15
- CardTitle,
16
- CardContent,
17
- } from '@object-ui/components';
18
- import { History, RotateCcw, Eye, ChevronRight } from 'lucide-react';
19
-
20
- export interface RevisionEntry {
21
- id: string;
22
- timestamp: string;
23
- user: string;
24
- changes: { field: string; oldValue: any; newValue: any }[];
25
- /** Full record snapshot at this revision point. */
26
- snapshot?: Record<string, any>;
27
- }
28
-
29
- export interface PointInTimeRestoreProps {
30
- recordId: string;
31
- revisions: RevisionEntry[];
32
- onRestore?: (revisionId: string, snapshot: Record<string, any>) => void | Promise<void>;
33
- className?: string;
34
- }
35
-
36
- function formatTimestamp(timestamp: string): string {
37
- try {
38
- const date = new Date(timestamp);
39
- const now = new Date();
40
- const diffMs = now.getTime() - date.getTime();
41
- const diffMins = Math.floor(diffMs / 60000);
42
-
43
- if (diffMins < 1) return 'just now';
44
- if (diffMins < 60) return `${diffMins}m ago`;
45
- const diffHours = Math.floor(diffMins / 60);
46
- if (diffHours < 24) return `${diffHours}h ago`;
47
- return date.toLocaleString();
48
- } catch {
49
- return timestamp;
50
- }
51
- }
52
-
53
- export const PointInTimeRestore: React.FC<PointInTimeRestoreProps> = ({
54
- recordId: _recordId,
55
- revisions,
56
- onRestore,
57
- className,
58
- }) => {
59
- const [selectedRevisionId, setSelectedRevisionId] = React.useState<string | null>(null);
60
- const [isConfirming, setIsConfirming] = React.useState(false);
61
- const [isRestoring, setIsRestoring] = React.useState(false);
62
-
63
- const selectedRevision = React.useMemo(
64
- () => revisions.find((r) => r.id === selectedRevisionId) ?? null,
65
- [revisions, selectedRevisionId],
66
- );
67
-
68
- const handleRestore = React.useCallback(async () => {
69
- if (!selectedRevision || !onRestore) return;
70
-
71
- if (!isConfirming) {
72
- setIsConfirming(true);
73
- return;
74
- }
75
-
76
- setIsRestoring(true);
77
- try {
78
- const snapshot = selectedRevision.snapshot ?? {};
79
- await onRestore(selectedRevision.id, snapshot);
80
- setIsConfirming(false);
81
- setSelectedRevisionId(null);
82
- } finally {
83
- setIsRestoring(false);
84
- }
85
- }, [selectedRevision, onRestore, isConfirming]);
86
-
87
- const handleCancelConfirm = React.useCallback(() => {
88
- setIsConfirming(false);
89
- }, []);
90
-
91
- return (
92
- <Card className={cn('', className)}>
93
- <CardHeader className="pb-3">
94
- <CardTitle className="flex items-center gap-2 text-base">
95
- <History className="h-4 w-4" />
96
- Revision History
97
- <span className="text-sm font-normal text-muted-foreground">
98
- ({revisions.length})
99
- </span>
100
- </CardTitle>
101
- </CardHeader>
102
- <CardContent>
103
- {revisions.length === 0 ? (
104
- <p className="text-sm text-muted-foreground text-center py-4">
105
- No revisions recorded
106
- </p>
107
- ) : (
108
- <div className="flex flex-col lg:flex-row gap-4">
109
- {/* Timeline */}
110
- <div className="flex-1 min-w-0">
111
- <div className="relative">
112
- <div className="absolute left-3 top-2 bottom-2 w-px bg-border" />
113
- <div className="space-y-1">
114
- {revisions.map((revision) => {
115
- const isSelected = revision.id === selectedRevisionId;
116
- return (
117
- <button
118
- key={revision.id}
119
- type="button"
120
- className={cn(
121
- 'w-full text-left flex items-start gap-3 py-2 px-2 rounded-md transition-colors relative',
122
- isSelected
123
- ? 'bg-accent'
124
- : 'hover:bg-accent/50',
125
- )}
126
- onClick={() => {
127
- setSelectedRevisionId(revision.id);
128
- setIsConfirming(false);
129
- }}
130
- >
131
- {/* Timeline dot */}
132
- <div
133
- className={cn(
134
- 'shrink-0 h-6 w-6 rounded-full border-2 flex items-center justify-center z-10 mt-0.5',
135
- isSelected
136
- ? 'border-primary bg-primary'
137
- : 'border-border bg-background',
138
- )}
139
- >
140
- {isSelected && (
141
- <ChevronRight className="h-3 w-3 text-primary-foreground" />
142
- )}
143
- </div>
144
- {/* Content */}
145
- <div className="flex-1 min-w-0">
146
- <div className="flex items-center gap-2">
147
- <span className="text-sm font-medium">{revision.user}</span>
148
- <span className="text-xs text-muted-foreground">
149
- {formatTimestamp(revision.timestamp)}
150
- </span>
151
- </div>
152
- <p className="text-xs text-muted-foreground mt-0.5">
153
- {revision.changes.length} field{revision.changes.length !== 1 ? 's' : ''}{' '}
154
- changed
155
- </p>
156
- </div>
157
- </button>
158
- );
159
- })}
160
- </div>
161
- </div>
162
- </div>
163
-
164
- {/* Preview panel */}
165
- {selectedRevision && (
166
- <div className="lg:w-80 border rounded-md p-3 space-y-3">
167
- <div className="flex items-center gap-2 text-sm font-medium">
168
- <Eye className="h-4 w-4 text-muted-foreground" />
169
- Revision Preview
170
- </div>
171
-
172
- {/* Field changes */}
173
- <div className="space-y-2">
174
- {selectedRevision.changes.map((change, i) => (
175
- <div key={i} className="text-xs">
176
- <span className="font-medium text-muted-foreground">
177
- {change.field}
178
- </span>
179
- <div className="flex items-center gap-1.5 mt-0.5">
180
- <span className="line-through text-red-600 dark:text-red-400 truncate max-w-[120px]">
181
- {change.oldValue != null ? String(change.oldValue) : '(empty)'}
182
- </span>
183
- <ChevronRight className="h-3 w-3 text-muted-foreground shrink-0" />
184
- <span className="text-green-600 dark:text-green-400 truncate max-w-[120px]">
185
- {change.newValue != null ? String(change.newValue) : '(empty)'}
186
- </span>
187
- </div>
188
- </div>
189
- ))}
190
- </div>
191
-
192
- {/* Snapshot values */}
193
- {selectedRevision.snapshot && (
194
- <div className="border-t pt-2 space-y-1">
195
- <p className="text-xs font-medium text-muted-foreground">
196
- Record state at this point
197
- </p>
198
- <div className="max-h-40 overflow-y-auto space-y-1">
199
- {Object.entries(selectedRevision.snapshot).map(([key, val]) => (
200
- <div key={key} className="flex justify-between text-xs gap-2">
201
- <span className="text-muted-foreground truncate">{key}</span>
202
- <span className="font-mono truncate max-w-[140px]">
203
- {val != null ? String(val) : '–'}
204
- </span>
205
- </div>
206
- ))}
207
- </div>
208
- </div>
209
- )}
210
-
211
- {/* Restore button */}
212
- {onRestore && (
213
- <div className="pt-1 space-y-2">
214
- {isConfirming ? (
215
- <>
216
- <p className="text-xs text-amber-600 dark:text-amber-400">
217
- This will restore the record to its state at{' '}
218
- {formatTimestamp(selectedRevision.timestamp)}. Continue?
219
- </p>
220
- <div className="flex gap-2">
221
- <Button
222
- variant="destructive"
223
- size="sm"
224
- className="gap-1.5 flex-1"
225
- onClick={handleRestore}
226
- disabled={isRestoring}
227
- >
228
- <RotateCcw className="h-3.5 w-3.5" />
229
- {isRestoring ? 'Restoring…' : 'Confirm Restore'}
230
- </Button>
231
- <Button
232
- variant="ghost"
233
- size="sm"
234
- onClick={handleCancelConfirm}
235
- disabled={isRestoring}
236
- >
237
- Cancel
238
- </Button>
239
- </div>
240
- </>
241
- ) : (
242
- <Button
243
- variant="outline"
244
- size="sm"
245
- className="w-full gap-1.5"
246
- onClick={handleRestore}
247
- >
248
- <RotateCcw className="h-3.5 w-3.5" />
249
- Restore to this point
250
- </Button>
251
- )}
252
- </div>
253
- )}
254
- </div>
255
- )}
256
- </div>
257
- )}
258
- </CardContent>
259
- </Card>
260
- );
261
- };
@@ -1,106 +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 { SmilePlus } from 'lucide-react';
12
- import type { Reaction } from '@object-ui/types';
13
-
14
- const DEFAULT_EMOJI_OPTIONS = ['👍', '❤️', '🎉', '😂', '😮', '😢'];
15
-
16
- export interface ReactionPickerProps {
17
- /** Existing reactions on the feed item */
18
- reactions: Reaction[];
19
- /** Called when user adds or removes a reaction */
20
- onToggleReaction?: (emoji: string) => void | Promise<void>;
21
- /** Available emoji options */
22
- emojiOptions?: string[];
23
- className?: string;
24
- }
25
-
26
- /**
27
- * ReactionPicker — Emoji reaction selector and display.
28
- * Shows existing reactions with counts, and a picker to add/remove.
29
- * Aligned with @objectstack/spec ReactionSchema.
30
- */
31
- export const ReactionPicker: React.FC<ReactionPickerProps> = ({
32
- reactions,
33
- onToggleReaction,
34
- emojiOptions = DEFAULT_EMOJI_OPTIONS,
35
- className,
36
- }) => {
37
- const [showPicker, setShowPicker] = React.useState(false);
38
-
39
- const handleReaction = React.useCallback(
40
- (emoji: string) => {
41
- onToggleReaction?.(emoji);
42
- setShowPicker(false);
43
- },
44
- [onToggleReaction],
45
- );
46
-
47
- return (
48
- <div className={cn('flex items-center gap-1 flex-wrap', className)}>
49
- {/* Existing reactions */}
50
- {reactions.map((reaction) => (
51
- <button
52
- key={reaction.emoji}
53
- type="button"
54
- className={cn(
55
- 'inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs border transition-colors',
56
- reaction.reacted
57
- ? 'bg-primary/10 border-primary/30 text-primary'
58
- : 'bg-muted border-border text-muted-foreground hover:bg-muted/80',
59
- )}
60
- onClick={() => handleReaction(reaction.emoji)}
61
- disabled={!onToggleReaction}
62
- aria-label={`${reaction.emoji} ${reaction.count} reaction${reaction.count !== 1 ? 's' : ''}`}
63
- >
64
- <span>{reaction.emoji}</span>
65
- <span>{reaction.count}</span>
66
- </button>
67
- ))}
68
-
69
- {/* Add reaction button */}
70
- {onToggleReaction && (
71
- <div className="relative">
72
- <Button
73
- variant="ghost"
74
- size="icon"
75
- className="h-6 w-6"
76
- onClick={() => setShowPicker(!showPicker)}
77
- aria-label="Add reaction"
78
- >
79
- <SmilePlus className="h-3.5 w-3.5" />
80
- </Button>
81
-
82
- {showPicker && (
83
- <div
84
- className="absolute bottom-full mb-1 left-0 bg-popover border rounded-md shadow-md z-50 p-1.5 flex gap-1"
85
- role="listbox"
86
- aria-label="Emoji picker"
87
- >
88
- {emojiOptions.map((emoji) => (
89
- <button
90
- key={emoji}
91
- type="button"
92
- className="hover:bg-accent rounded p-1 text-base transition-colors"
93
- onClick={() => handleReaction(emoji)}
94
- role="option"
95
- aria-selected={reactions.some(r => r.emoji === emoji && r.reacted)}
96
- >
97
- {emoji}
98
- </button>
99
- ))}
100
- </div>
101
- )}
102
- </div>
103
- )}
104
- </div>
105
- );
106
- };