@open-mercato/core 0.6.6-develop.6519.1.bdc91fb755 → 0.6.6-develop.6520.1.55df6df792

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 (70) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/customers/backend/calendar/page.js +9 -1
  3. package/dist/modules/customers/backend/calendar/page.js.map +2 -2
  4. package/dist/modules/customers/components/calendar/AgendaList.js +0 -11
  5. package/dist/modules/customers/components/calendar/AgendaList.js.map +2 -2
  6. package/dist/modules/customers/components/calendar/CalendarEventEditor.js +391 -235
  7. package/dist/modules/customers/components/calendar/CalendarEventEditor.js.map +2 -2
  8. package/dist/modules/customers/components/calendar/CalendarScreen.js +5 -5
  9. package/dist/modules/customers/components/calendar/CalendarScreen.js.map +2 -2
  10. package/dist/modules/customers/components/calendar/CalendarSettingsModal.js +2 -2
  11. package/dist/modules/customers/components/calendar/CalendarSettingsModal.js.map +2 -2
  12. package/dist/modules/customers/components/calendar/MonthGrid.js +1 -11
  13. package/dist/modules/customers/components/calendar/MonthGrid.js.map +2 -2
  14. package/dist/modules/customers/components/calendar/TimeGrid.js +162 -176
  15. package/dist/modules/customers/components/calendar/TimeGrid.js.map +2 -2
  16. package/dist/modules/customers/components/calendar/editor/LocationField.js +61 -14
  17. package/dist/modules/customers/components/calendar/editor/LocationField.js.map +2 -2
  18. package/dist/modules/customers/components/calendar/editor/PeopleField.js +10 -5
  19. package/dist/modules/customers/components/calendar/editor/PeopleField.js.map +2 -2
  20. package/dist/modules/customers/components/calendar/editor/PriorityField.js +85 -0
  21. package/dist/modules/customers/components/calendar/editor/PriorityField.js.map +7 -0
  22. package/dist/modules/customers/components/calendar/editor/RelatedToField.js +8 -3
  23. package/dist/modules/customers/components/calendar/editor/RelatedToField.js.map +2 -2
  24. package/dist/modules/customers/components/calendar/editor/ResourcesField.js +225 -0
  25. package/dist/modules/customers/components/calendar/editor/ResourcesField.js.map +7 -0
  26. package/dist/modules/customers/components/calendar/editor/SegmentGroup.js +12 -7
  27. package/dist/modules/customers/components/calendar/editor/SegmentGroup.js.map +2 -2
  28. package/dist/modules/customers/components/calendar/editor/inputs.js +68 -57
  29. package/dist/modules/customers/components/calendar/editor/inputs.js.map +2 -2
  30. package/dist/modules/customers/components/calendar/editor/lookups.js +82 -2
  31. package/dist/modules/customers/components/calendar/editor/lookups.js.map +2 -2
  32. package/dist/modules/customers/components/calendar/useCalendarItems.js +6 -1
  33. package/dist/modules/customers/components/calendar/useCalendarItems.js.map +2 -2
  34. package/dist/modules/customers/data/validators.js +3 -1
  35. package/dist/modules/customers/data/validators.js.map +2 -2
  36. package/dist/modules/customers/lib/calendar/editorPayload.js +53 -22
  37. package/dist/modules/customers/lib/calendar/editorPayload.js.map +2 -2
  38. package/dist/modules/entities/backend/entities/user/[entityId]/page.js +1 -1
  39. package/dist/modules/entities/backend/entities/user/[entityId]/page.js.map +2 -2
  40. package/package.json +7 -7
  41. package/src/modules/customers/backend/calendar/page.tsx +8 -1
  42. package/src/modules/customers/components/calendar/AgendaList.tsx +0 -10
  43. package/src/modules/customers/components/calendar/CalendarEventEditor.tsx +444 -261
  44. package/src/modules/customers/components/calendar/CalendarScreen.tsx +12 -5
  45. package/src/modules/customers/components/calendar/CalendarSettingsModal.tsx +6 -6
  46. package/src/modules/customers/components/calendar/MonthGrid.tsx +0 -11
  47. package/src/modules/customers/components/calendar/TimeGrid.tsx +1 -24
  48. package/src/modules/customers/components/calendar/editor/LocationField.tsx +64 -2
  49. package/src/modules/customers/components/calendar/editor/PeopleField.tsx +11 -4
  50. package/src/modules/customers/components/calendar/editor/PriorityField.tsx +90 -0
  51. package/src/modules/customers/components/calendar/editor/RelatedToField.tsx +8 -3
  52. package/src/modules/customers/components/calendar/editor/ResourcesField.tsx +264 -0
  53. package/src/modules/customers/components/calendar/editor/SegmentGroup.tsx +14 -6
  54. package/src/modules/customers/components/calendar/editor/inputs.tsx +91 -50
  55. package/src/modules/customers/components/calendar/editor/lookups.ts +115 -2
  56. package/src/modules/customers/components/calendar/useCalendarItems.ts +8 -1
  57. package/src/modules/customers/data/validators.ts +3 -1
  58. package/src/modules/customers/i18n/de.json +12 -2
  59. package/src/modules/customers/i18n/en.json +12 -2
  60. package/src/modules/customers/i18n/es.json +12 -2
  61. package/src/modules/customers/i18n/pl.json +12 -2
  62. package/src/modules/customers/lib/calendar/editorPayload.ts +94 -37
  63. package/src/modules/entities/backend/entities/user/[entityId]/page.tsx +1 -1
  64. package/src/modules/entities/i18n/de.json +120 -0
  65. package/src/modules/entities/i18n/en.json +120 -0
  66. package/src/modules/entities/i18n/es.json +120 -0
  67. package/src/modules/entities/i18n/pl.json +120 -0
  68. package/dist/modules/customers/components/calendar/editor/CategoryField.js +0 -75
  69. package/dist/modules/customers/components/calendar/editor/CategoryField.js.map +0 -7
  70. package/src/modules/customers/components/calendar/editor/CategoryField.tsx +0 -79
@@ -107,7 +107,14 @@ function asEditableItem(item: CalendarItem): CalendarItem {
107
107
  return item.isRecurringOccurrence ? { ...item, id: item.raw.id } : item
108
108
  }
109
109
 
110
- export function CalendarScreen() {
110
+ export type CalendarScreenProps = {
111
+ /** True when the optional resources module is loaded (server-resolved). */
112
+ resourcesEnabled?: boolean
113
+ /** True when the optional staff module is loaded (server-resolved). */
114
+ staffEnabled?: boolean
115
+ }
116
+
117
+ export function CalendarScreen({ resourcesEnabled = false, staffEnabled = true }: CalendarScreenProps = {}) {
111
118
  const t = useT()
112
119
  const [view, setView] = React.useState<CalendarView>('week')
113
120
  const [anchor, setAnchor] = React.useState<Date>(() => new Date())
@@ -136,7 +143,7 @@ export function CalendarScreen() {
136
143
  () => getVisibleRange(view, anchor, agendaHorizonDays),
137
144
  [view, anchor, agendaHorizonDays],
138
145
  )
139
- const { items, isLoading, error, truncated, typeLabels, typeColors, refetch } = useCalendarItems(range)
146
+ const { items, isLoading, error, truncated, typeLabels, typeColors, typeIcons, refetch } = useCalendarItems(range)
140
147
 
141
148
  React.useEffect(() => {
142
149
  if (!isLoading) setHasLoadedOnce(true)
@@ -577,11 +584,11 @@ export function CalendarScreen() {
577
584
  defaultDate={anchor}
578
585
  defaultRange={createRange}
579
586
  typeLabels={typeLabels}
580
- typeColors={typeColors}
581
- surfacedTypes={preferences.activityTypes}
582
- eventCategories={preferences.eventCategories}
587
+ typeIcons={typeIcons}
583
588
  conflictScope={preferences.conflictScope}
584
589
  currentUserId={currentUserId}
590
+ resourcesEnabled={resourcesEnabled}
591
+ staffEnabled={staffEnabled}
585
592
  onOpenChange={(open) => setEditor((current) => ({ ...current, open }))}
586
593
  onSaved={refetch}
587
594
  />
@@ -30,10 +30,10 @@ export type CalendarSettingsModalProps = {
30
30
  type ToggleKey = 'showCrmActivities' | 'aiSummaries' | 'conflictWarnings' | 'showWeekends'
31
31
 
32
32
  // An empty Activity Types list is an intentional floor meaning "surface all
33
- // dictionary types" (see buildEditorCategoryOptions) rather than "surface none"
34
- // it keeps the editor's category picker usable. So when the stored list is empty
35
- // the modal seeds the dictionary types for display; curating a non-empty subset
36
- // persists and filters the editor as expected.
33
+ // dictionary types" rather than "surface none". When the stored list is empty
34
+ // the modal seeds the dictionary types for display. NOTE: since the editor's
35
+ // Category quick-pick was removed (owner feedback, #3552) these lists no longer
36
+ // affect the event editor — the type switcher always shows the full dictionary.
37
37
  function buildDraft(preferences: CalendarPreferences, seedActivityTypes: string[]): CalendarPreferences {
38
38
  return {
39
39
  ...preferences,
@@ -162,8 +162,8 @@ export function CalendarSettingsModal({
162
162
  value={draft.conflictScope}
163
163
  onChange={(conflictScope) => setDraft((current) => ({ ...current, conflictScope }))}
164
164
  options={[
165
- { value: 'mine', label: t('customers.calendar.settings.conflictScopeMine', 'My meetings only') },
166
- { value: 'all', label: t('customers.calendar.settings.conflictScopeAll', 'All org meetings') },
165
+ { value: 'mine', label: t('customers.calendar.settings.conflictScopeMine', 'My meetings') },
166
+ { value: 'all', label: t('customers.calendar.settings.conflictScopeAll', 'All meetings') },
167
167
  ]}
168
168
  />
169
169
  </div>
@@ -5,11 +5,9 @@ import { addDays } from 'date-fns/addDays'
5
5
  import { format } from 'date-fns/format'
6
6
  import { isSameMonth } from 'date-fns/isSameMonth'
7
7
  import { isToday } from 'date-fns/isToday'
8
- import { CalendarDays } from 'lucide-react'
9
8
  import { useT } from '@open-mercato/shared/lib/i18n/context'
10
9
  import { cn } from '@open-mercato/shared/lib/utils'
11
10
  import { Button } from '@open-mercato/ui/primitives/button'
12
- import { EmptyState } from '@open-mercato/ui/primitives/empty-state'
13
11
  import { getVisibleRange } from '../../lib/calendar/range'
14
12
  import { eventDisplayTitle } from '../../lib/calendar/labels'
15
13
  import type { CalendarItem, MonthGridProps } from './types'
@@ -225,15 +223,6 @@ export function MonthGrid({ anchor, items, onItemClick, onDayOpen }: MonthGridPr
225
223
  ))}
226
224
  </div>
227
225
  ))}
228
- {items.length === 0 ? (
229
- <div className="pointer-events-none absolute inset-0 z-10 flex items-center justify-center p-6">
230
- <EmptyState
231
- icon={<CalendarDays className="h-6 w-6" aria-hidden="true" />}
232
- title={t('customers.calendar.empty.month', 'Nothing scheduled this month')}
233
- className="bg-background/90"
234
- />
235
- </div>
236
- ) : null}
237
226
  </div>
238
227
  )
239
228
  }
@@ -1,7 +1,7 @@
1
1
  "use client"
2
2
 
3
3
  import * as React from 'react'
4
- import { CalendarDays, ChevronLeft, ChevronRight } from 'lucide-react'
4
+ import { ChevronLeft, ChevronRight } from 'lucide-react'
5
5
  import { addDays } from 'date-fns/addDays'
6
6
  import { isSameDay } from 'date-fns/isSameDay'
7
7
  import { startOfDay } from 'date-fns/startOfDay'
@@ -9,7 +9,6 @@ import { cn } from '@open-mercato/shared/lib/utils'
9
9
  import { useLocale, useT } from '@open-mercato/shared/lib/i18n/context'
10
10
  import { Button } from '@open-mercato/ui/primitives/button'
11
11
  import { IconButton } from '@open-mercato/ui/primitives/icon-button'
12
- import { EmptyState } from '@open-mercato/ui/primitives/empty-state'
13
12
  import { packOverlaps } from '../../lib/calendar/layout'
14
13
  import { getVisibleRange } from '../../lib/calendar/range'
15
14
  import {
@@ -165,7 +164,6 @@ export function TimeGrid({
165
164
  onItemClick,
166
165
  onJoin,
167
166
  onNavigate,
168
- onCreate,
169
167
  onCreateRange,
170
168
  }: TimeGridProps) {
171
169
  const t = useT()
@@ -488,27 +486,6 @@ export function TimeGrid({
488
486
  })}
489
487
  </div>
490
488
  </div>
491
- {items.length === 0 ? (
492
- <div className="pointer-events-none absolute inset-0 z-40 flex items-center justify-center p-6">
493
- <EmptyState
494
- className="pointer-events-auto bg-card/95 shadow-sm"
495
- icon={<CalendarDays className="h-6 w-6" aria-hidden />}
496
- title={
497
- days === 7
498
- ? t('customers.calendar.empty.week', 'Nothing scheduled this week')
499
- : t('customers.calendar.empty.day', 'Nothing scheduled this day')
500
- }
501
- description={t('customers.calendar.empty.description', 'Plan a meeting, event or task to fill your week.')}
502
- actions={
503
- onCreate ? (
504
- <Button type="button" size="sm" onClick={onCreate}>
505
- {t('customers.calendar.actions.newEvent', 'New event')}
506
- </Button>
507
- ) : undefined
508
- }
509
- />
510
- </div>
511
- ) : null}
512
489
  </div>
513
490
  )
514
491
  }
@@ -1,19 +1,76 @@
1
1
  "use client"
2
2
 
3
3
  import * as React from 'react'
4
- import { MapPin } from 'lucide-react'
4
+ import { MapPin, Phone, Plus } from 'lucide-react'
5
5
  import { useT } from '@open-mercato/shared/lib/i18n/context'
6
6
  import { Input } from '@open-mercato/ui/primitives/input'
7
7
  import { Field } from './inputs'
8
+ import { fetchPeoplePhones, type ContactPhone } from './lookups'
9
+
10
+ // Below the Call "Phone / link" field: when a linked contact has a primary
11
+ // phone and the field is still empty, offer it as a one-click suggestion chip
12
+ // showing the actual number (#3552). Companies/staff have no phone and never
13
+ // appear. The chips vanish once the field is filled.
14
+ function PhoneSuggestions({
15
+ contactIds,
16
+ onPick,
17
+ }: {
18
+ contactIds: string[]
19
+ onPick(phone: string): void
20
+ }) {
21
+ const t = useT()
22
+ const [contacts, setContacts] = React.useState<ContactPhone[]>([])
23
+ const idsKey = contactIds.join(',')
24
+
25
+ React.useEffect(() => {
26
+ if (contactIds.length === 0) {
27
+ setContacts([])
28
+ return
29
+ }
30
+ const controller = new AbortController()
31
+ let cancelled = false
32
+ fetchPeoplePhones(contactIds, controller.signal).then((found) => {
33
+ if (!cancelled) setContacts(found)
34
+ })
35
+ return () => {
36
+ cancelled = true
37
+ controller.abort()
38
+ }
39
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- idsKey captures the ids
40
+ }, [idsKey])
41
+
42
+ if (contacts.length === 0) return null
43
+ return (
44
+ <div className="flex flex-wrap items-center gap-1.5 pt-1.5">
45
+ {contacts.map((contact) => (
46
+ <button
47
+ key={contact.id}
48
+ type="button"
49
+ onClick={() => onPick(contact.phone)}
50
+ title={t('customers.calendar.editor.phoneSuggestion.use', 'Use {name}’s number', { name: contact.name })}
51
+ className="inline-flex max-w-full items-center gap-1.5 rounded-md border border-border bg-background px-2 py-1 text-xs text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
52
+ >
53
+ <Plus aria-hidden className="size-3 shrink-0" />
54
+ <span className="shrink-0">{t('customers.calendar.editor.phoneSuggestion.add', 'Add')}</span>
55
+ <span className="truncate">{contact.name}</span>
56
+ <span className="shrink-0 tabular-nums text-foreground">{contact.phone}</span>
57
+ </button>
58
+ ))}
59
+ </div>
60
+ )
61
+ }
8
62
 
9
63
  export function LocationField({
10
64
  variant,
11
65
  value,
12
66
  onChange,
67
+ phoneContactIds,
13
68
  }: {
14
69
  variant: 'location' | 'phoneLink'
15
70
  value: string
16
71
  onChange(next: string): void
72
+ /** Person ids whose phone can be suggested (Call variant only). */
73
+ phoneContactIds?: string[]
17
74
  }) {
18
75
  const t = useT()
19
76
  const label = variant === 'phoneLink'
@@ -22,6 +79,8 @@ export function LocationField({
22
79
  const placeholder = variant === 'phoneLink'
23
80
  ? t('customers.calendar.editor.phoneLinkPlaceholder', 'Add a phone number or link…')
24
81
  : t('customers.calendar.editor.locationPlaceholder', 'Add a location or link…')
82
+ // Suggestions only help while the field is empty — once filled they'd be noise.
83
+ const showSuggestions = variant === 'phoneLink' && value.trim().length === 0
25
84
  return (
26
85
  <Field label={label}>
27
86
  <Input
@@ -30,8 +89,11 @@ export function LocationField({
30
89
  onChange={(event) => onChange(event.target.value)}
31
90
  placeholder={placeholder}
32
91
  aria-label={label}
33
- leftIcon={<MapPin />}
92
+ leftIcon={variant === 'phoneLink' ? <Phone /> : <MapPin />}
34
93
  />
94
+ {showSuggestions ? (
95
+ <PhoneSuggestions contactIds={phoneContactIds ?? []} onPick={(phone) => onChange(phone)} />
96
+ ) : null}
35
97
  </Field>
36
98
  )
37
99
  }
@@ -9,7 +9,7 @@ import { Input } from '@open-mercato/ui/primitives/input'
9
9
  import type { EditorParticipant } from '../../../lib/calendar/editorPayload'
10
10
  import { composeAccessibleName } from '../../../lib/calendar/labels'
11
11
  import { searchPeopleOptions, type PersonOption } from './lookups'
12
- import { CONTROL_BORDER, DROPDOWN_PANEL_CLASS, PersonChip, UppercaseBadge } from './inputs'
12
+ import { CONTROL_BORDER, DROPDOWN_PANEL_CLASS, PersonChip, UppercaseBadge, useDropdownDismiss } from './inputs'
13
13
 
14
14
  export function PeopleField({
15
15
  mode,
@@ -18,6 +18,7 @@ export function PeopleField({
18
18
  value,
19
19
  onChange,
20
20
  includeCustomers,
21
+ includeStaff = true,
21
22
  }: {
22
23
  mode: 'multi' | 'single'
23
24
  placeholder: string
@@ -25,12 +26,16 @@ export function PeopleField({
25
26
  value: EditorParticipant[]
26
27
  onChange(next: EditorParticipant[]): void
27
28
  includeCustomers: boolean
29
+ /** Pass false when the staff module is not loaded — customer-only options. */
30
+ includeStaff?: boolean
28
31
  }) {
29
32
  const t = useT()
30
33
  const [query, setQuery] = React.useState('')
31
34
  const [open, setOpen] = React.useState(false)
32
35
  const [options, setOptions] = React.useState<PersonOption[]>([])
33
36
  const [loading, setLoading] = React.useState(false)
37
+ const close = React.useCallback(() => setOpen(false), [])
38
+ const rootRef = useDropdownDismiss(open, close)
34
39
 
35
40
  React.useEffect(() => {
36
41
  if (!open) return
@@ -39,7 +44,7 @@ export function PeopleField({
39
44
  const timer = window.setTimeout(async () => {
40
45
  setLoading(true)
41
46
  try {
42
- const results = await searchPeopleOptions(query.trim(), { includeCustomers, signal: controller.signal })
47
+ const results = await searchPeopleOptions(query.trim(), { includeCustomers, includeStaff, signal: controller.signal })
43
48
  if (cancelled) return
44
49
  setOptions(results)
45
50
  } catch {
@@ -53,7 +58,7 @@ export function PeopleField({
53
58
  controller.abort()
54
59
  window.clearTimeout(timer)
55
60
  }
56
- }, [open, query, includeCustomers])
61
+ }, [open, query, includeCustomers, includeStaff])
57
62
 
58
63
  const selectedIds = new Set(value.map((participant) => participant.userId))
59
64
  const visibleOptions = options.filter((option) => !selectedIds.has(option.userId))
@@ -66,6 +71,7 @@ export function PeopleField({
66
71
 
67
72
  return (
68
73
  <div
74
+ ref={rootRef}
69
75
  className="relative w-full"
70
76
  onBlur={(event) => {
71
77
  if (!event.currentTarget.contains(event.relatedTarget as Node | null)) setOpen(false)
@@ -74,7 +80,7 @@ export function PeopleField({
74
80
  <div
75
81
  className={cn(
76
82
  'flex w-full flex-wrap content-center items-center gap-2 rounded-md bg-background px-2.5 py-2',
77
- mode === 'multi' ? 'min-h-24' : 'min-h-14',
83
+ 'min-h-14',
78
84
  CONTROL_BORDER,
79
85
  )}
80
86
  >
@@ -125,6 +131,7 @@ export function PeopleField({
125
131
  option.email,
126
132
  option.isCustomer ? t('customers.calendar.editor.customerBadge', 'Customer') : null,
127
133
  ])}
134
+ title={composeAccessibleName([option.name, option.email])}
128
135
  onClick={() => {
129
136
  const participant: EditorParticipant = {
130
137
  userId: option.userId,
@@ -0,0 +1,90 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import { Check, ChevronDown, ChevronsDown, ChevronsUp, Equal } from 'lucide-react'
5
+ import { cn } from '@open-mercato/shared/lib/utils'
6
+ import { Button } from '@open-mercato/ui/primitives/button'
7
+ import type { EditorPriority } from '../../../lib/calendar/editorPayload'
8
+ import { CONTROL_BORDER, DROPDOWN_PANEL_CLASS, useDropdownDismiss } from './inputs'
9
+
10
+ // Jira/Linear-style priority glyphs: filled double chevrons for the extremes,
11
+ // an equals bar for the middle. Colours stay on DS status tokens (no amber):
12
+ // blue = low urgency, muted = medium, red = high.
13
+ const PRIORITY_META: Record<EditorPriority, { Icon: React.ComponentType<{ className?: string }>; color: string }> = {
14
+ low: { Icon: ChevronsDown, color: 'text-status-info-text' },
15
+ medium: { Icon: Equal, color: 'text-muted-foreground' },
16
+ high: { Icon: ChevronsUp, color: 'text-status-error-text' },
17
+ }
18
+
19
+ const PRIORITY_ORDER: EditorPriority[] = ['high', 'medium', 'low']
20
+
21
+ export function PriorityField({
22
+ value,
23
+ labels,
24
+ ariaLabel,
25
+ onChange,
26
+ }: {
27
+ value: EditorPriority
28
+ labels: Record<EditorPriority, string>
29
+ ariaLabel: string
30
+ onChange(next: EditorPriority): void
31
+ }) {
32
+ const [open, setOpen] = React.useState(false)
33
+ const close = React.useCallback(() => setOpen(false), [])
34
+ const rootRef = useDropdownDismiss(open, close)
35
+ const selected = PRIORITY_META[value]
36
+ const SelectedIcon = selected.Icon
37
+
38
+ return (
39
+ <div
40
+ ref={rootRef}
41
+ className="relative w-full"
42
+ onBlur={(event) => {
43
+ if (!event.currentTarget.contains(event.relatedTarget as Node | null)) setOpen(false)
44
+ }}
45
+ >
46
+ <Button
47
+ type="button"
48
+ variant="outline"
49
+ aria-haspopup="listbox"
50
+ aria-expanded={open}
51
+ aria-label={ariaLabel}
52
+ onClick={() => setOpen((previous) => !previous)}
53
+ className={cn('h-9 w-full justify-between bg-background px-3 font-normal shadow-none', CONTROL_BORDER)}
54
+ >
55
+ <span className="flex min-w-0 items-center gap-2">
56
+ <SelectedIcon aria-hidden className={cn('size-4 shrink-0', selected.color)} />
57
+ <span className="truncate text-sm text-foreground">{labels[value]}</span>
58
+ </span>
59
+ <ChevronDown aria-hidden className="size-4 shrink-0 opacity-60" />
60
+ </Button>
61
+ {open ? (
62
+ <div role="listbox" aria-label={ariaLabel} className={DROPDOWN_PANEL_CLASS}>
63
+ {PRIORITY_ORDER.map((priority) => {
64
+ const meta = PRIORITY_META[priority]
65
+ const Icon = meta.Icon
66
+ const active = priority === value
67
+ return (
68
+ <Button
69
+ key={priority}
70
+ type="button"
71
+ variant="ghost"
72
+ role="option"
73
+ aria-selected={active}
74
+ onClick={() => {
75
+ onChange(priority)
76
+ setOpen(false)
77
+ }}
78
+ className={cn('h-auto w-full justify-start gap-2 px-2 py-1.5 text-left text-sm font-normal', active && 'bg-muted')}
79
+ >
80
+ <Icon aria-hidden className={cn('size-4 shrink-0', meta.color)} />
81
+ <span className="min-w-0 flex-1 truncate text-foreground">{labels[priority]}</span>
82
+ <Check aria-hidden className={cn('size-4 shrink-0', active ? 'opacity-100' : 'opacity-0')} />
83
+ </Button>
84
+ )
85
+ })}
86
+ </div>
87
+ ) : null}
88
+ </div>
89
+ )
90
+ }
@@ -15,7 +15,7 @@ import {
15
15
  type DealOption,
16
16
  type RelatedEntityOption,
17
17
  } from './lookups'
18
- import { CONTROL_BORDER, DROPDOWN_PANEL_CLASS, PersonChip, UppercaseBadge } from './inputs'
18
+ import { CONTROL_BORDER, DROPDOWN_PANEL_CLASS, PersonChip, UppercaseBadge, useDropdownDismiss } from './inputs'
19
19
 
20
20
  const OPTION_ROW_CLASS =
21
21
  'h-auto w-full justify-start gap-2 whitespace-normal px-2 py-1.5 text-left text-sm font-normal text-foreground'
@@ -37,6 +37,8 @@ export function RelatedToField({
37
37
  }) {
38
38
  const t = useT()
39
39
  const [open, setOpen] = React.useState(false)
40
+ const closeDropdown = React.useCallback(() => setOpen(false), [])
41
+ const rootRef = useDropdownDismiss(open, closeDropdown)
40
42
  const [query, setQuery] = React.useState('')
41
43
  const [options, setOptions] = React.useState<RelatedEntityOption[]>([])
42
44
  const [deals, setDeals] = React.useState<DealOption[]>([])
@@ -92,6 +94,7 @@ export function RelatedToField({
92
94
 
93
95
  return (
94
96
  <div
97
+ ref={rootRef}
95
98
  className="relative w-full"
96
99
  onBlur={(event) => {
97
100
  if (!event.currentTarget.contains(event.relatedTarget as Node | null)) setOpen(false)
@@ -99,7 +102,7 @@ export function RelatedToField({
99
102
  >
100
103
  <div
101
104
  className={cn(
102
- 'flex h-10 w-full items-center rounded-md bg-background pl-2.5 pr-3',
105
+ 'flex h-10 w-full items-center rounded-md bg-background pl-2.5 pr-3 transition-colors hover:bg-accent/50',
103
106
  error ? 'border border-status-error-border' : CONTROL_BORDER,
104
107
  )}
105
108
  >
@@ -110,7 +113,7 @@ export function RelatedToField({
110
113
  aria-expanded={open}
111
114
  aria-label={label}
112
115
  onClick={() => setOpen((previous) => !previous)}
113
- className="h-full min-w-0 flex-1 justify-between rounded-none bg-transparent p-0 text-left shadow-none hover:bg-transparent"
116
+ className="h-full min-w-0 flex-1 justify-between rounded-none bg-transparent p-0 text-left shadow-none hover:bg-transparent dark:hover:bg-transparent"
114
117
  >
115
118
  <span className="flex min-w-0 items-center gap-2">
116
119
  {value ? (
@@ -180,6 +183,7 @@ export function RelatedToField({
180
183
  role="option"
181
184
  aria-selected={value?.id === option.id}
182
185
  aria-label={composeAccessibleName([option.label, option.subtitle, kindLabel(option.kind)])}
186
+ title={composeAccessibleName([option.label, option.subtitle])}
183
187
  onClick={() => {
184
188
  onChange({ id: option.id, kind: option.kind, label: option.label })
185
189
  if (value?.id !== option.id) onDealChange(null)
@@ -223,6 +227,7 @@ export function RelatedToField({
223
227
  variant="ghost"
224
228
  role="option"
225
229
  aria-selected={deal?.id === option.id}
230
+ title={option.label}
226
231
  onClick={() => {
227
232
  onDealChange(option)
228
233
  setOpen(false)