@proteos/ui 0.51.0 → 0.51.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.
- package/dist/index.cjs +36 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -2
- package/dist/index.d.ts +10 -2
- package/dist/index.js +37 -14
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/conversation-brief-card/conversation-brief-preview.tsx +47 -10
- package/src/components/conversation-brief-card/conversation-brief-review.test.tsx +36 -0
- package/src/components/conversation-brief-card/conversation-brief-review.tsx +5 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@proteos/ui",
|
|
3
|
-
"version": "0.51.
|
|
3
|
+
"version": "0.51.1",
|
|
4
4
|
"license": "FSL-1.1-Apache-2.0",
|
|
5
5
|
"description": "Proteos React component library (React Aria Components + Tailwind v4).",
|
|
6
6
|
"repository": {
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"tailwind-variants": "^3.2.2",
|
|
57
57
|
"tailwindcss": "^4.2.4",
|
|
58
58
|
"thinking-orbs": "^0.3.1",
|
|
59
|
-
"@proteos/sdk": "0.51.
|
|
59
|
+
"@proteos/sdk": "0.51.1"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@biomejs/biome": "^2.4.13",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Contact, ConversationBrief } from '@proteos/sdk'
|
|
2
|
-
import { Phone } from 'lucide-react'
|
|
2
|
+
import { Pencil, Phone } from 'lucide-react'
|
|
3
3
|
import { type ReactNode, useEffect, useMemo, useRef, useState } from 'react'
|
|
4
|
+
import { Button as AriaButton } from 'react-aria-components'
|
|
4
5
|
|
|
5
6
|
import { cn } from '../../lib/cn'
|
|
6
7
|
import { Button } from '../button'
|
|
@@ -15,6 +16,13 @@ import { contactPhoneNumbers } from './types'
|
|
|
15
16
|
it is a read-only preview (a custom component's own modal, the hub's call
|
|
16
17
|
panel). The host may slot extra controls (a caller-id picker) via
|
|
17
18
|
`controls`.
|
|
19
|
+
|
|
20
|
+
The guidance text is markdown-canonical but the lib carries no markdown
|
|
21
|
+
renderer: hosts inject one via `renderContent` (the web app passes its
|
|
22
|
+
MarkdownContent); without it the text shows as plain pre-wrapped lines.
|
|
23
|
+
Editing follows the same seam — `onEdit` reveals a pencil on hover over
|
|
24
|
+
the guidance box, and `editor` lets the host swap the box for its own
|
|
25
|
+
editing surface while a draft is open.
|
|
18
26
|
========================================================================= */
|
|
19
27
|
|
|
20
28
|
export interface ConversationBriefPreviewProps {
|
|
@@ -44,6 +52,12 @@ export interface ConversationBriefPreviewProps {
|
|
|
44
52
|
className?: string
|
|
45
53
|
/** Visual override for the guidance text box (e.g. a max height). */
|
|
46
54
|
contentClassName?: string
|
|
55
|
+
/** Renders the guidance text (markdown). Absent ⇒ plain pre-wrapped text. */
|
|
56
|
+
renderContent?: (content: string) => ReactNode
|
|
57
|
+
/** Shows an Edit button on hover over the guidance box. */
|
|
58
|
+
onEdit?: () => void
|
|
59
|
+
/** Replaces the guidance box wholesale — the host's editing surface. */
|
|
60
|
+
editor?: ReactNode
|
|
47
61
|
}
|
|
48
62
|
|
|
49
63
|
export function ConversationBriefPreview({
|
|
@@ -58,6 +72,9 @@ export function ConversationBriefPreview({
|
|
|
58
72
|
callLabel = 'Call',
|
|
59
73
|
className,
|
|
60
74
|
contentClassName,
|
|
75
|
+
renderContent,
|
|
76
|
+
onEdit,
|
|
77
|
+
editor,
|
|
61
78
|
}: ConversationBriefPreviewProps) {
|
|
62
79
|
const [pickedContactId, setPickedContactId] = useState('')
|
|
63
80
|
const contact = contacts.find((entry) => entry.id === pickedContactId) ?? contacts[0] ?? null
|
|
@@ -99,15 +116,35 @@ export function ConversationBriefPreview({
|
|
|
99
116
|
.join(' · ')}
|
|
100
117
|
</p>
|
|
101
118
|
</div>
|
|
102
|
-
|
|
103
|
-
className=
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
119
|
+
{editor ?? (
|
|
120
|
+
<div className="group/brief-content relative">
|
|
121
|
+
<div
|
|
122
|
+
className={cn(
|
|
123
|
+
'rounded-md border border-rule-2 bg-surface-2/60 px-3 py-2.5',
|
|
124
|
+
'text-[13px] leading-relaxed text-ink-2 overflow-y-auto [scrollbar-width:thin]',
|
|
125
|
+
renderContent ? null : 'whitespace-pre-wrap',
|
|
126
|
+
contentClassName,
|
|
127
|
+
)}
|
|
128
|
+
>
|
|
129
|
+
{renderContent ? renderContent(brief.content) : brief.content}
|
|
130
|
+
</div>
|
|
131
|
+
{onEdit ? (
|
|
132
|
+
<AriaButton
|
|
133
|
+
aria-label="Edit brief"
|
|
134
|
+
onPress={onEdit}
|
|
135
|
+
className={cn(
|
|
136
|
+
'absolute right-1.5 top-1.5 inline-flex size-7 cursor-pointer items-center justify-center',
|
|
137
|
+
'rounded-md bg-surface/80 text-ink-3 outline-none transition-opacity',
|
|
138
|
+
'opacity-0 group-hover/brief-content:opacity-100 focus-visible:opacity-100',
|
|
139
|
+
'data-[hovered]:bg-surface-3 data-[hovered]:text-ink',
|
|
140
|
+
'focus-visible:shadow-[var(--shadow-focus)]',
|
|
141
|
+
)}
|
|
142
|
+
>
|
|
143
|
+
<Pencil className="size-3.5" aria-hidden />
|
|
144
|
+
</AriaButton>
|
|
145
|
+
) : null}
|
|
146
|
+
</div>
|
|
147
|
+
)}
|
|
111
148
|
{onCall ? (
|
|
112
149
|
<div className="flex flex-col gap-2 border-t border-rule-2 pt-3">
|
|
113
150
|
{contacts.length > 1 ? (
|
|
@@ -212,3 +212,39 @@ describe('ConversationBriefPreview', () => {
|
|
|
212
212
|
expect(screen.getByText(/no phone number/)).toBeInTheDocument()
|
|
213
213
|
})
|
|
214
214
|
})
|
|
215
|
+
|
|
216
|
+
describe('ConversationBriefPreview — content seam', () => {
|
|
217
|
+
it('renders the guidance through renderContent when provided', () => {
|
|
218
|
+
render(
|
|
219
|
+
<ConversationBriefPreview
|
|
220
|
+
brief={brief({ content: '**bold** guidance' })}
|
|
221
|
+
contacts={[contact()]}
|
|
222
|
+
renderContent={(content) => <em data-testid="rendered">{content.toUpperCase()}</em>}
|
|
223
|
+
/>,
|
|
224
|
+
)
|
|
225
|
+
expect(screen.getByTestId('rendered')).toHaveTextContent('**BOLD** GUIDANCE')
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
it('offers an Edit button only when onEdit is given', async () => {
|
|
229
|
+
const onEdit = vi.fn()
|
|
230
|
+
const { rerender } = render(<ConversationBriefPreview brief={brief()} contacts={[contact()]} />)
|
|
231
|
+
expect(screen.queryByRole('button', { name: 'Edit brief' })).toBeNull()
|
|
232
|
+
rerender(<ConversationBriefPreview brief={brief()} contacts={[contact()]} onEdit={onEdit} />)
|
|
233
|
+
await userEvent.click(screen.getByRole('button', { name: 'Edit brief' }))
|
|
234
|
+
expect(onEdit).toHaveBeenCalledTimes(1)
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
it('swaps the guidance box for the host editor', () => {
|
|
238
|
+
render(
|
|
239
|
+
<ConversationBriefPreview
|
|
240
|
+
brief={brief()}
|
|
241
|
+
contacts={[contact()]}
|
|
242
|
+
onEdit={() => {}}
|
|
243
|
+
editor={<textarea aria-label="Draft" defaultValue="editing" />}
|
|
244
|
+
/>,
|
|
245
|
+
)
|
|
246
|
+
expect(screen.getByRole('textbox', { name: 'Draft' })).toBeInTheDocument()
|
|
247
|
+
expect(screen.queryByText('Ask about the renewal.', { exact: false })).toBeNull()
|
|
248
|
+
expect(screen.queryByRole('button', { name: 'Edit brief' })).toBeNull()
|
|
249
|
+
})
|
|
250
|
+
})
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Contact, ConversationBrief } from '@proteos/sdk'
|
|
2
|
-
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
2
|
+
import { type ReactNode, useCallback, useEffect, useRef, useState } from 'react'
|
|
3
3
|
|
|
4
4
|
import { ModalBody, ModalContent, ModalHeader, ModalTitle } from '../modal'
|
|
5
5
|
import { ConversationBriefCard } from './conversation-brief-card'
|
|
@@ -53,6 +53,8 @@ export interface ConversationBriefReviewProps {
|
|
|
53
53
|
overlayClassName?: string
|
|
54
54
|
openLabel?: string
|
|
55
55
|
className?: string
|
|
56
|
+
/** Built-in modal only: markdown renderer for the guidance text. */
|
|
57
|
+
renderContent?: (content: string) => ReactNode
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
export function ConversationBriefReview({
|
|
@@ -69,6 +71,7 @@ export function ConversationBriefReview({
|
|
|
69
71
|
overlayClassName,
|
|
70
72
|
openLabel,
|
|
71
73
|
className,
|
|
74
|
+
renderContent,
|
|
72
75
|
}: ConversationBriefReviewProps) {
|
|
73
76
|
const [brief, setBrief] = useState<ConversationBrief | null>(null)
|
|
74
77
|
const [contacts, setContacts] = useState<Contact[]>([])
|
|
@@ -149,6 +152,7 @@ export function ConversationBriefReview({
|
|
|
149
152
|
contentClassName="max-h-[50dvh]"
|
|
150
153
|
isCallDisabled={isCallDisabled}
|
|
151
154
|
{...(callDisabledReason !== undefined ? { callDisabledReason } : {})}
|
|
155
|
+
{...(renderContent !== undefined ? { renderContent } : {})}
|
|
152
156
|
{...(onCall && isDialableConversationBrief(brief)
|
|
153
157
|
? {
|
|
154
158
|
onCall: (contact: Contact, phoneNumber: string) => {
|