@open-mercato/ui 0.5.1-develop.2663.2c29774b5b → 0.5.1-develop.2681.c559bb2bc3

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 (53) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/dist/backend/CrudForm.js +187 -39
  3. package/dist/backend/CrudForm.js.map +2 -2
  4. package/dist/backend/Page.js +12 -4
  5. package/dist/backend/Page.js.map +2 -2
  6. package/dist/backend/confirm-dialog/ConfirmDialog.js +7 -4
  7. package/dist/backend/confirm-dialog/ConfirmDialog.js.map +2 -2
  8. package/dist/backend/crud/CollapsibleGroup.js +88 -0
  9. package/dist/backend/crud/CollapsibleGroup.js.map +7 -0
  10. package/dist/backend/crud/CollapsibleZoneLayout.js +178 -0
  11. package/dist/backend/crud/CollapsibleZoneLayout.js.map +7 -0
  12. package/dist/backend/crud/useGroupCollapse.js +24 -0
  13. package/dist/backend/crud/useGroupCollapse.js.map +7 -0
  14. package/dist/backend/crud/useGroupOrder.js +61 -0
  15. package/dist/backend/crud/useGroupOrder.js.map +7 -0
  16. package/dist/backend/crud/usePersistedBooleanFlag.js +29 -0
  17. package/dist/backend/crud/usePersistedBooleanFlag.js.map +7 -0
  18. package/dist/backend/crud/useZoneCollapse.js +24 -0
  19. package/dist/backend/crud/useZoneCollapse.js.map +7 -0
  20. package/dist/backend/detail/AttachmentsSection.js +77 -33
  21. package/dist/backend/detail/AttachmentsSection.js.map +2 -2
  22. package/dist/backend/detail/NotesSection.js +82 -6
  23. package/dist/backend/detail/NotesSection.js.map +2 -2
  24. package/dist/backend/icons/lucideRegistry.generated.js +16 -2
  25. package/dist/backend/icons/lucideRegistry.generated.js.map +2 -2
  26. package/dist/backend/inputs/SwitchableMarkdownInput.js +3 -1
  27. package/dist/backend/inputs/SwitchableMarkdownInput.js.map +2 -2
  28. package/dist/primitives/avatar.js +59 -0
  29. package/dist/primitives/avatar.js.map +7 -0
  30. package/package.json +3 -3
  31. package/src/backend/CrudForm.tsx +230 -21
  32. package/src/backend/Page.tsx +20 -4
  33. package/src/backend/__tests__/AttachmentsSection.test.tsx +82 -0
  34. package/src/backend/__tests__/CollapsibleZoneLayout.test.tsx +171 -0
  35. package/src/backend/__tests__/CrudForm.validation.test.tsx +4 -4
  36. package/src/backend/__tests__/NotesSection.test.tsx +63 -0
  37. package/src/backend/confirm-dialog/ConfirmDialog.tsx +9 -4
  38. package/src/backend/crud/CollapsibleGroup.tsx +111 -0
  39. package/src/backend/crud/CollapsibleZoneLayout.tsx +234 -0
  40. package/src/backend/crud/__tests__/useGroupCollapse.test.ts +38 -0
  41. package/src/backend/crud/__tests__/useGroupOrder.test.ts +63 -0
  42. package/src/backend/crud/__tests__/usePersistedBooleanFlag.test.ts +49 -0
  43. package/src/backend/crud/__tests__/useZoneCollapse.test.ts +31 -0
  44. package/src/backend/crud/useGroupCollapse.ts +22 -0
  45. package/src/backend/crud/useGroupOrder.ts +74 -0
  46. package/src/backend/crud/usePersistedBooleanFlag.ts +35 -0
  47. package/src/backend/crud/useZoneCollapse.ts +22 -0
  48. package/src/backend/detail/AttachmentsSection.tsx +81 -38
  49. package/src/backend/detail/NotesSection.tsx +99 -6
  50. package/src/backend/icons/lucideRegistry.generated.tsx +16 -2
  51. package/src/backend/inputs/SwitchableMarkdownInput.tsx +3 -1
  52. package/src/primitives/__tests__/avatar.test.tsx +64 -0
  53. package/src/primitives/avatar.tsx +75 -0
@@ -64,6 +64,19 @@ export type NotesUpdatePayload = {
64
64
 
65
65
  export type NotesDataAdapter<C = unknown> = {
66
66
  list: (params: { entityId: string | null; dealId: string | null; context?: C }) => Promise<CommentSummary[]>
67
+ listPage?: (params: {
68
+ entityId: string | null
69
+ dealId: string | null
70
+ page: number
71
+ pageSize: number
72
+ context?: C
73
+ }) => Promise<{
74
+ items: CommentSummary[]
75
+ total: number
76
+ page: number
77
+ pageSize: number
78
+ totalPages: number
79
+ }>
67
80
  create: (params: NotesCreatePayload & { context?: C }) => Promise<Partial<CommentSummary> | void>
68
81
  update: (params: { id: string; patch: NotesUpdatePayload; context?: C }) => Promise<void>
69
82
  delete: (params: { id: string; context?: C }) => Promise<void>
@@ -446,7 +459,10 @@ function NotesSectionImpl<C = unknown>({
446
459
  const [contentError, setContentError] = React.useState<string | null>(null)
447
460
  const contentTextareaRef = React.useRef<HTMLTextAreaElement | null>(null)
448
461
  const [visibleCount, setVisibleCount] = React.useState(0)
462
+ const [currentPage, setCurrentPage] = React.useState(1)
463
+ const [totalPages, setTotalPages] = React.useState(1)
449
464
  const [deletingNoteId, setDeletingNoteId] = React.useState<string | null>(null)
465
+ const pagedMode = typeof dataAdapter.listPage === 'function'
450
466
 
451
467
  React.useEffect(() => {
452
468
  const queryEntityId = typeof entityId === 'string' ? entityId : ''
@@ -455,6 +471,8 @@ function NotesSectionImpl<C = unknown>({
455
471
  setNotes([])
456
472
  setLoadError(null)
457
473
  setIsLoading(false)
474
+ setCurrentPage(1)
475
+ setTotalPages(1)
458
476
  return
459
477
  }
460
478
  let cancelled = false
@@ -463,6 +481,20 @@ function NotesSectionImpl<C = unknown>({
463
481
  pushLoading()
464
482
  async function loadNotes() {
465
483
  try {
484
+ if (dataAdapter.listPage) {
485
+ const pageResult = await dataAdapter.listPage({
486
+ entityId: queryEntityId || null,
487
+ dealId: queryDealId || null,
488
+ page: 1,
489
+ pageSize: 20,
490
+ context: dataContext,
491
+ })
492
+ if (cancelled) return
493
+ setNotes(pageResult.items)
494
+ setCurrentPage(pageResult.page)
495
+ setTotalPages(pageResult.totalPages)
496
+ return
497
+ }
466
498
  const mapped = await dataAdapter.list({
467
499
  entityId: queryEntityId || null,
468
500
  dealId: queryDealId || null,
@@ -470,12 +502,16 @@ function NotesSectionImpl<C = unknown>({
470
502
  })
471
503
  if (cancelled) return
472
504
  setNotes(mapped)
505
+ setCurrentPage(1)
506
+ setTotalPages(1)
473
507
  } catch (err) {
474
508
  if (cancelled) return
475
509
  const message =
476
510
  err instanceof Error ? err.message : label('loadError', 'Failed to load notes.')
477
511
  setNotes([])
478
512
  setLoadError(message)
513
+ setCurrentPage(1)
514
+ setTotalPages(1)
479
515
  flash(message, 'error')
480
516
  } finally {
481
517
  if (!cancelled) setIsLoading(false)
@@ -486,7 +522,7 @@ function NotesSectionImpl<C = unknown>({
486
522
  return () => {
487
523
  cancelled = true
488
524
  }
489
- }, [dataAdapter, dataContext, dealId, entityId, popLoading, pushLoading, t])
525
+ }, [dataAdapter, dataContext, dealId, entityId, label, popLoading, pushLoading])
490
526
 
491
527
  const youLabel = label('you', 'You')
492
528
  const viewerLabel = React.useMemo(() => viewerName ?? viewerEmail ?? null, [viewerEmail, viewerName])
@@ -534,6 +570,10 @@ function NotesSectionImpl<C = unknown>({
534
570
  }, [readMarkdownPreference])
535
571
 
536
572
  React.useEffect(() => {
573
+ if (pagedMode) {
574
+ setVisibleCount(notes.length)
575
+ return
576
+ }
537
577
  if (!notes.length) {
538
578
  setVisibleCount(0)
539
579
  return
@@ -543,7 +583,7 @@ function NotesSectionImpl<C = unknown>({
543
583
  if (prev >= notes.length) return prev
544
584
  return Math.min(Math.max(prev, baseline), notes.length)
545
585
  })
546
- }, [notes.length])
586
+ }, [notes.length, pagedMode])
547
587
 
548
588
  React.useEffect(() => {
549
589
  if (hasEntity) return
@@ -553,8 +593,14 @@ function NotesSectionImpl<C = unknown>({
553
593
  setDraftColor(null)
554
594
  }, [hasEntity])
555
595
 
556
- const visibleNotes = React.useMemo(() => notes.slice(0, visibleCount), [notes, visibleCount])
557
- const hasVisibleNotes = React.useMemo(() => visibleCount > 0, [visibleCount])
596
+ const visibleNotes = React.useMemo(
597
+ () => (pagedMode ? notes : notes.slice(0, visibleCount)),
598
+ [notes, pagedMode, visibleCount],
599
+ )
600
+ const hasVisibleNotes = React.useMemo(
601
+ () => (pagedMode ? notes.length > 0 : visibleCount > 0),
602
+ [notes.length, pagedMode, visibleCount],
603
+ )
558
604
 
559
605
  const loadMoreLabel = label('loadMore')
560
606
 
@@ -618,6 +664,7 @@ function NotesSectionImpl<C = unknown>({
618
664
  }
619
665
  return [newNote, ...prev]
620
666
  })
667
+ setVisibleCount((prev) => Math.max(prev, 1))
621
668
  flash(label('success'), 'success')
622
669
  return true
623
670
  } catch (err) {
@@ -686,6 +733,9 @@ function NotesSectionImpl<C = unknown>({
686
733
  try {
687
734
  await dataAdapter.delete({ id: note.id, context: dataContext })
688
735
  setNotes((prev) => prev.filter((existing) => existing.id !== note.id))
736
+ if (pagedMode) {
737
+ setVisibleCount((prev) => Math.max(0, prev - 1))
738
+ }
689
739
  flash(label('deleteSuccess', 'Note deleted'), 'success')
690
740
  } catch (err) {
691
741
  const message = err instanceof Error ? err.message : label('deleteError', 'Failed to delete note')
@@ -716,11 +766,40 @@ function NotesSectionImpl<C = unknown>({
716
766
  )
717
767
 
718
768
  const handleLoadMore = React.useCallback(() => {
769
+ if (pagedMode && dataAdapter.listPage) {
770
+ if (currentPage >= totalPages || isLoading) return
771
+ const queryEntityId = typeof entityId === 'string' ? entityId : ''
772
+ const queryDealId = typeof dealId === 'string' ? dealId : ''
773
+ setIsLoading(true)
774
+ pushLoading()
775
+ void dataAdapter.listPage({
776
+ entityId: queryEntityId || null,
777
+ dealId: queryDealId || null,
778
+ page: currentPage + 1,
779
+ pageSize: 20,
780
+ context: dataContext,
781
+ })
782
+ .then((pageResult) => {
783
+ setNotes((prev) => [...prev, ...pageResult.items])
784
+ setCurrentPage(pageResult.page)
785
+ setTotalPages(pageResult.totalPages)
786
+ })
787
+ .catch((error) => {
788
+ const message =
789
+ error instanceof Error ? error.message : label('loadError', 'Failed to load notes.')
790
+ flash(message, 'error')
791
+ })
792
+ .finally(() => {
793
+ setIsLoading(false)
794
+ popLoading()
795
+ })
796
+ return
797
+ }
719
798
  setVisibleCount((prev) => {
720
799
  if (prev >= notes.length) return prev
721
800
  return Math.min(prev + 5, notes.length)
722
801
  })
723
- }, [notes.length])
802
+ }, [currentPage, dataAdapter, dataContext, dealId, entityId, flash, isLoading, label, notes.length, pagedMode, popLoading, pushLoading, totalPages])
724
803
 
725
804
  const handleAppearanceDialogSubmit = React.useCallback(async () => {
726
805
  if (!appearanceDialogState) return
@@ -1034,6 +1113,20 @@ function NotesSectionImpl<C = unknown>({
1034
1113
  {loadError ? <ErrorMessage label={loadError} className="mt-3" /> : null}
1035
1114
 
1036
1115
  <div className="space-y-3">
1116
+ {!composerOpen && hasVisibleNotes && !onActionChange ? (
1117
+ <div className="flex justify-end">
1118
+ <Button
1119
+ type="button"
1120
+ variant="outline"
1121
+ size="sm"
1122
+ onClick={focusComposer}
1123
+ disabled={isSubmitting || isLoading || !hasEntity}
1124
+ >
1125
+ <Plus className="size-4" />
1126
+ {addActionLabel}
1127
+ </Button>
1128
+ </div>
1129
+ ) : null}
1037
1130
  {isLoading ? (
1038
1131
  <LoadingMessage
1039
1132
  label={label('loading', 'Loading notes…')}
@@ -1206,7 +1299,7 @@ function NotesSectionImpl<C = unknown>({
1206
1299
  }}
1207
1300
  />
1208
1301
  )}
1209
- {isLoading || visibleCount >= notes.length ? null : (
1302
+ {isLoading || (pagedMode ? currentPage >= totalPages : visibleCount >= notes.length) ? null : (
1210
1303
  <div className="flex justify-center">
1211
1304
  <Button variant="outline" size="sm" onClick={handleLoadMore}>
1212
1305
  {loadMoreLabel}
@@ -43,15 +43,18 @@ import {
43
43
  Coins,
44
44
  Copy,
45
45
  CreditCard,
46
+ Crown,
46
47
  Database,
47
48
  DollarSign,
48
49
  Download,
49
50
  ExternalLink,
51
+ Eye,
50
52
  FileMinus,
51
53
  FilePenLine,
52
54
  FileText,
53
55
  FilterX,
54
56
  Flag,
57
+ Flame,
55
58
  FolderTree,
56
59
  Gauge,
57
60
  GitBranch,
@@ -61,7 +64,9 @@ import {
61
64
  GraduationCap,
62
65
  Hand,
63
66
  Handshake,
67
+ Headphones,
64
68
  Heart,
69
+ HelpCircle,
65
70
  Hourglass,
66
71
  Inbox,
67
72
  Key,
@@ -77,7 +82,7 @@ import {
77
82
  Mail,
78
83
  MailOpen,
79
84
  MapPin,
80
- Megaphone,
85
+ MinusCircle,
81
86
  Notebook,
82
87
  Package,
83
88
  PackageCheck,
@@ -106,6 +111,7 @@ import {
106
111
  Shuffle,
107
112
  Sliders,
108
113
  Smartphone,
114
+ Snowflake,
109
115
  Sparkles,
110
116
  Star,
111
117
  StickyNote,
@@ -127,6 +133,7 @@ import {
127
133
  UserPlus,
128
134
  UserRound,
129
135
  Users,
136
+ UserX,
130
137
  Wallet,
131
138
  Webhook,
132
139
  Wrench,
@@ -173,15 +180,18 @@ export const LUCIDE_ICON_REGISTRY: Record<string, LucideIcon> = {
173
180
  'coins': Coins,
174
181
  'copy': Copy,
175
182
  'credit-card': CreditCard,
183
+ 'crown': Crown,
176
184
  'database': Database,
177
185
  'dollar-sign': DollarSign,
178
186
  'download': Download,
179
187
  'external-link': ExternalLink,
188
+ 'eye': Eye,
180
189
  'file-minus': FileMinus,
181
190
  'file-pen-line': FilePenLine,
182
191
  'file-text': FileText,
183
192
  'filter-x': FilterX,
184
193
  'flag': Flag,
194
+ 'flame': Flame,
185
195
  'folder-tree': FolderTree,
186
196
  'gauge': Gauge,
187
197
  'git-branch': GitBranch,
@@ -191,7 +201,9 @@ export const LUCIDE_ICON_REGISTRY: Record<string, LucideIcon> = {
191
201
  'graduation-cap': GraduationCap,
192
202
  'hand': Hand,
193
203
  'handshake': Handshake,
204
+ 'headphones': Headphones,
194
205
  'heart': Heart,
206
+ 'help-circle': HelpCircle,
195
207
  'hourglass': Hourglass,
196
208
  'inbox': Inbox,
197
209
  'key': Key,
@@ -207,7 +219,7 @@ export const LUCIDE_ICON_REGISTRY: Record<string, LucideIcon> = {
207
219
  'mail': Mail,
208
220
  'mail-open': MailOpen,
209
221
  'map-pin': MapPin,
210
- 'megaphone': Megaphone,
222
+ 'minus-circle': MinusCircle,
211
223
  'notebook': Notebook,
212
224
  'package': Package,
213
225
  'package-check': PackageCheck,
@@ -236,6 +248,7 @@ export const LUCIDE_ICON_REGISTRY: Record<string, LucideIcon> = {
236
248
  'shuffle': Shuffle,
237
249
  'sliders': Sliders,
238
250
  'smartphone': Smartphone,
251
+ 'snowflake': Snowflake,
239
252
  'sparkles': Sparkles,
240
253
  'star': Star,
241
254
  'sticky-note': StickyNote,
@@ -256,6 +269,7 @@ export const LUCIDE_ICON_REGISTRY: Record<string, LucideIcon> = {
256
269
  'user-minus': UserMinus,
257
270
  'user-plus': UserPlus,
258
271
  'user-round': UserRound,
272
+ 'user-x': UserX,
259
273
  'users': Users,
260
274
  'wallet': Wallet,
261
275
  'webhook': Webhook,
@@ -5,6 +5,7 @@ import dynamic from 'next/dynamic'
5
5
  import type { PluggableList } from 'unified'
6
6
  import { LoadingMessage } from '../detail/LoadingMessage'
7
7
  import { useMarkdownRemarkPlugins } from '../markdown/useMarkdownRemarkPlugins'
8
+ import { useTheme } from '../../theme'
8
9
 
9
10
  export type SwitchableMarkdownInputProps = {
10
11
  value: string
@@ -69,6 +70,7 @@ export function SwitchableMarkdownInput({
69
70
  remarkPlugins,
70
71
  }: SwitchableMarkdownInputProps) {
71
72
  const resolvedPlugins = useMarkdownRemarkPlugins(remarkPlugins)
73
+ const { resolvedTheme } = useTheme()
72
74
  const editorWrapperClasses =
73
75
  editorWrapperClassName ?? 'w-full rounded-lg border border-muted-foreground/20 bg-background p-2'
74
76
  const editorClasses = editorClassName ?? 'w-full'
@@ -79,7 +81,7 @@ export function SwitchableMarkdownInput({
79
81
  if (isMarkdownEnabled && !disableMarkdown) {
80
82
  return (
81
83
  <div className={editorWrapperClasses}>
82
- <div data-color-mode="light" className={editorClasses}>
84
+ <div data-color-mode={resolvedTheme} className={editorClasses}>
83
85
  <UiMarkdownEditor
84
86
  value={value}
85
87
  height={height}
@@ -0,0 +1,64 @@
1
+ import * as React from 'react'
2
+ import { render, screen } from '@testing-library/react'
3
+ import { Avatar } from '../avatar'
4
+
5
+ describe('Avatar', () => {
6
+ it('renders two-character initials for multi-word labels', () => {
7
+ render(<Avatar label="Jan Kowalski" />)
8
+ expect(screen.getByText('JK')).toBeInTheDocument()
9
+ })
10
+
11
+ it('renders first two characters for single-word labels', () => {
12
+ render(<Avatar label="Acme" />)
13
+ expect(screen.getByText('AC')).toBeInTheDocument()
14
+ })
15
+
16
+ it('renders a question mark when label is empty', () => {
17
+ render(<Avatar label=" " />)
18
+ expect(screen.getByText('?')).toBeInTheDocument()
19
+ })
20
+
21
+ it('uses the provided ariaLabel over the label for a11y', () => {
22
+ render(<Avatar label="Jan Kowalski" ariaLabel="Owner avatar" />)
23
+ expect(screen.getByRole('img', { name: 'Owner avatar' })).toBeInTheDocument()
24
+ })
25
+
26
+ it('falls back to label for aria when ariaLabel is not provided', () => {
27
+ render(<Avatar label="Acme Corp" />)
28
+ expect(screen.getByRole('img', { name: 'Acme Corp' })).toBeInTheDocument()
29
+ })
30
+
31
+ it('applies monochrome classes when variant=monochrome', () => {
32
+ const { container } = render(<Avatar label="Jan" variant="monochrome" />)
33
+ const root = container.firstChild as HTMLElement
34
+ expect(root.className).toContain('bg-muted')
35
+ expect(root.className).toContain('text-muted-foreground')
36
+ })
37
+
38
+ it('applies default (colored) classes when variant is default', () => {
39
+ const { container } = render(<Avatar label="Jan" />)
40
+ const root = container.firstChild as HTMLElement
41
+ expect(root.className).toContain('bg-primary/10')
42
+ expect(root.className).toContain('text-primary')
43
+ })
44
+
45
+ it('renders an icon in place of initials when icon is provided', () => {
46
+ const Icon = () => <svg data-testid="bldg" />
47
+ render(<Avatar label="Acme" icon={<Icon />} />)
48
+ expect(screen.getByTestId('bldg')).toBeInTheDocument()
49
+ expect(screen.queryByText('AC')).not.toBeInTheDocument()
50
+ })
51
+
52
+ it('renders an image when src is provided', () => {
53
+ const { container } = render(<Avatar label="Jan" src="/avatar.png" />)
54
+ const img = container.querySelector('img')
55
+ expect(img).toBeTruthy()
56
+ expect(img?.getAttribute('src')).toBe('/avatar.png')
57
+ })
58
+
59
+ it('applies size classes from size prop', () => {
60
+ const { container } = render(<Avatar label="Jan" size="lg" />)
61
+ const root = container.firstChild as HTMLElement
62
+ expect(root.className).toContain('size-12')
63
+ })
64
+ })
@@ -0,0 +1,75 @@
1
+ import * as React from 'react'
2
+ import { cva, type VariantProps } from 'class-variance-authority'
3
+ import { cn } from '@open-mercato/shared/lib/utils'
4
+
5
+ const avatarVariants = cva(
6
+ 'inline-flex shrink-0 items-center justify-center overflow-hidden rounded-full font-semibold select-none',
7
+ {
8
+ variants: {
9
+ size: {
10
+ xs: 'size-5 text-xs',
11
+ sm: 'size-7 text-xs',
12
+ md: 'size-9 text-sm',
13
+ lg: 'size-12 text-base',
14
+ xl: 'size-16 text-xl',
15
+ },
16
+ variant: {
17
+ default: 'bg-primary/10 text-primary',
18
+ monochrome: 'bg-muted text-muted-foreground',
19
+ },
20
+ },
21
+ defaultVariants: {
22
+ size: 'md',
23
+ variant: 'default',
24
+ },
25
+ },
26
+ )
27
+
28
+ function computeInitials(label: string): string {
29
+ const trimmed = label.trim()
30
+ if (!trimmed.length) return '?'
31
+ const parts = trimmed.split(/\s+/).filter(Boolean)
32
+ if (parts.length === 1) {
33
+ return parts[0].slice(0, 2).toUpperCase()
34
+ }
35
+ const first = parts[0][0] ?? ''
36
+ const last = parts[parts.length - 1][0] ?? ''
37
+ return (first + last).toUpperCase()
38
+ }
39
+
40
+ export type AvatarProps = {
41
+ label: string
42
+ src?: string | null
43
+ icon?: React.ReactNode
44
+ ariaLabel?: string
45
+ } & VariantProps<typeof avatarVariants> &
46
+ Omit<React.HTMLAttributes<HTMLDivElement>, 'role' | 'aria-label'>
47
+
48
+ export const Avatar = React.forwardRef<HTMLDivElement, AvatarProps>(
49
+ ({ className, label, src, icon, size, variant, ariaLabel, ...rest }, ref) => {
50
+ const initials = React.useMemo(() => computeInitials(label), [label])
51
+ return (
52
+ <div
53
+ ref={ref}
54
+ role="img"
55
+ aria-label={ariaLabel ?? label}
56
+ className={cn(avatarVariants({ size, variant }), className)}
57
+ {...rest}
58
+ >
59
+ {src ? (
60
+ <img src={src} alt="" className="size-full object-cover" aria-hidden="true" />
61
+ ) : icon ? (
62
+ <span aria-hidden="true" className="flex items-center justify-center [&>svg]:size-[55%]">
63
+ {icon}
64
+ </span>
65
+ ) : (
66
+ <span aria-hidden="true">{initials}</span>
67
+ )}
68
+ </div>
69
+ )
70
+ },
71
+ )
72
+
73
+ Avatar.displayName = 'Avatar'
74
+
75
+ export { avatarVariants }