@flamingo-stack/openframe-frontend-core 0.0.172 → 0.0.173
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/{chunk-AY3E5FKM.cjs → chunk-2MEBOTV4.cjs} +2263 -1698
- package/dist/chunk-2MEBOTV4.cjs.map +1 -0
- package/dist/{chunk-FEEPEOW2.js → chunk-J3ZCNPDM.js} +6128 -5563
- package/dist/chunk-J3ZCNPDM.js.map +1 -0
- package/dist/components/chat/approval-batch-message.d.ts +10 -0
- package/dist/components/chat/approval-batch-message.d.ts.map +1 -0
- package/dist/components/chat/chat-input.d.ts.map +1 -1
- package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
- package/dist/components/chat/chat-message-list.d.ts.map +1 -1
- package/dist/components/chat/chat-message-loader.d.ts +23 -0
- package/dist/components/chat/chat-message-loader.d.ts.map +1 -0
- package/dist/components/chat/hooks/index.d.ts +1 -0
- package/dist/components/chat/hooks/index.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-collapsible.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-delayed-flag.d.ts +25 -0
- package/dist/components/chat/hooks/use-delayed-flag.d.ts.map +1 -0
- package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts.map +1 -1
- package/dist/components/chat/index.d.ts +3 -0
- package/dist/components/chat/index.d.ts.map +1 -1
- package/dist/components/chat/thinking-display.d.ts.map +1 -1
- package/dist/components/chat/tool-call-blocks.d.ts +18 -0
- package/dist/components/chat/tool-call-blocks.d.ts.map +1 -0
- package/dist/components/chat/tool-execution-display.d.ts +1 -1
- package/dist/components/chat/tool-execution-display.d.ts.map +1 -1
- package/dist/components/chat/types/api.types.d.ts +16 -1
- package/dist/components/chat/types/api.types.d.ts.map +1 -1
- package/dist/components/chat/types/component.types.d.ts +2 -2
- package/dist/components/chat/types/component.types.d.ts.map +1 -1
- package/dist/components/chat/types/message.types.d.ts +54 -1
- package/dist/components/chat/types/message.types.d.ts.map +1 -1
- package/dist/components/chat/types/network.types.d.ts +2 -0
- package/dist/components/chat/types/network.types.d.ts.map +1 -1
- package/dist/components/chat/types/processing.types.d.ts +14 -1
- package/dist/components/chat/types/processing.types.d.ts.map +1 -1
- package/dist/components/chat/utils/chunk-parser.d.ts.map +1 -1
- package/dist/components/chat/utils/extract-incomplete-message-state.d.ts.map +1 -1
- package/dist/components/chat/utils/index.d.ts +1 -0
- package/dist/components/chat/utils/index.d.ts.map +1 -1
- package/dist/components/chat/utils/message-segment-accumulator.d.ts +32 -4
- package/dist/components/chat/utils/message-segment-accumulator.d.ts.map +1 -1
- package/dist/components/chat/utils/process-historical-messages.d.ts +2 -1
- package/dist/components/chat/utils/process-historical-messages.d.ts.map +1 -1
- package/dist/components/chat/utils/tool-call-helpers.d.ts +38 -0
- package/dist/components/chat/utils/tool-call-helpers.d.ts.map +1 -0
- package/dist/components/features/index.cjs +2 -2
- package/dist/components/features/index.js +1 -1
- package/dist/components/index.cjs +14 -2
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +13 -1
- package/dist/components/navigation/index.cjs +2 -2
- package/dist/components/navigation/index.js +1 -1
- package/dist/components/ui/index.cjs +14 -2
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.js +13 -1
- package/dist/components/ui/textarea.d.ts +11 -0
- package/dist/components/ui/textarea.d.ts.map +1 -1
- package/dist/index.cjs +14 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +13 -1
- package/package.json +3 -3
- package/src/components/chat/approval-batch-message.tsx +237 -0
- package/src/components/chat/chat-input.tsx +34 -106
- package/src/components/chat/chat-message-enhanced.tsx +11 -0
- package/src/components/chat/chat-message-list.tsx +22 -6
- package/src/components/chat/chat-message-loader.tsx +67 -0
- package/src/components/chat/hooks/index.ts +1 -0
- package/src/components/chat/hooks/use-collapsible.ts +10 -1
- package/src/components/chat/hooks/use-delayed-flag.ts +56 -0
- package/src/components/chat/hooks/use-realtime-chunk-processor.ts +109 -20
- package/src/components/chat/index.ts +3 -0
- package/src/components/chat/thinking-display.tsx +76 -12
- package/src/components/chat/tool-call-blocks.tsx +58 -0
- package/src/components/chat/tool-execution-display.tsx +60 -81
- package/src/components/chat/types/api.types.ts +22 -3
- package/src/components/chat/types/component.types.ts +2 -2
- package/src/components/chat/types/message.types.ts +58 -1
- package/src/components/chat/types/network.types.ts +2 -0
- package/src/components/chat/types/processing.types.ts +13 -2
- package/src/components/chat/utils/chunk-parser.ts +40 -5
- package/src/components/chat/utils/extract-incomplete-message-state.ts +19 -1
- package/src/components/chat/utils/index.ts +3 -0
- package/src/components/chat/utils/message-segment-accumulator.ts +136 -12
- package/src/components/chat/utils/process-historical-messages.ts +88 -13
- package/src/components/chat/utils/tool-call-helpers.ts +105 -0
- package/src/components/ui/textarea.tsx +107 -25
- package/dist/chunk-AY3E5FKM.cjs.map +0 -1
- package/dist/chunk-FEEPEOW2.js.map +0 -1
|
@@ -7,13 +7,16 @@ import {
|
|
|
7
7
|
MESSAGE_TYPE,
|
|
8
8
|
OWNER_TYPE,
|
|
9
9
|
type AuthorType,
|
|
10
|
+
type ChatApprovalStatus,
|
|
10
11
|
type HistoricalMessage,
|
|
12
|
+
type PendingToolCallData,
|
|
11
13
|
type ProcessedMessage,
|
|
12
14
|
type MessageProcessingOptions,
|
|
13
15
|
type MessageData,
|
|
14
16
|
type MessageOwner,
|
|
15
17
|
} from '../types'
|
|
16
18
|
import { MessageSegmentAccumulator, createMessageSegmentAccumulator } from './message-segment-accumulator'
|
|
19
|
+
import { getCommandText } from './tool-call-helpers'
|
|
17
20
|
|
|
18
21
|
function getOwnerDisplayName(owner?: MessageOwner): string {
|
|
19
22
|
if (owner?.type === OWNER_TYPE.ADMIN && owner.user) {
|
|
@@ -48,7 +51,10 @@ function pushStandaloneMessages(
|
|
|
48
51
|
*/
|
|
49
52
|
export interface ProcessHistoricalMessagesResult {
|
|
50
53
|
messages: ProcessedMessage[]
|
|
51
|
-
escalatedApprovals: Map<
|
|
54
|
+
escalatedApprovals: Map<
|
|
55
|
+
string,
|
|
56
|
+
{ command: string; explanation?: string; approvalType: string; toolCalls?: PendingToolCallData[] }
|
|
57
|
+
>
|
|
52
58
|
}
|
|
53
59
|
|
|
54
60
|
/**
|
|
@@ -71,7 +77,10 @@ export function processHistoricalMessages(
|
|
|
71
77
|
|
|
72
78
|
const processedMessages: ProcessedMessage[] = []
|
|
73
79
|
const accumulator = createMessageSegmentAccumulator({ onApprove, onReject })
|
|
74
|
-
const escalatedApprovals = new Map<
|
|
80
|
+
const escalatedApprovals = new Map<
|
|
81
|
+
string,
|
|
82
|
+
{ command: string; explanation?: string; approvalType: string; toolCalls?: PendingToolCallData[] }
|
|
83
|
+
>()
|
|
75
84
|
|
|
76
85
|
let currentAssistantId: string | null = null
|
|
77
86
|
let currentAssistantTimestamp: Date | null = null
|
|
@@ -192,9 +201,15 @@ function processMessageData(
|
|
|
192
201
|
accumulator: MessageSegmentAccumulator,
|
|
193
202
|
approvalStatuses: Record<string, string>,
|
|
194
203
|
options: MessageProcessingOptions = {},
|
|
195
|
-
escalatedApprovals?: Map<
|
|
204
|
+
escalatedApprovals?: Map<
|
|
205
|
+
string,
|
|
206
|
+
{ command: string; explanation?: string; approvalType: string; toolCalls?: PendingToolCallData[] }
|
|
207
|
+
>
|
|
196
208
|
): void {
|
|
197
|
-
|
|
209
|
+
// batchApprovalsEnabled is owned by the consumer (oss-tenant chat client /
|
|
210
|
+
// openframe-frontend tickets). Defaults to ON so consumers that haven't
|
|
211
|
+
// wired the flag yet get the batch UI; pass `false` to force legacy.
|
|
212
|
+
const { displayApprovalTypes, batchApprovalsEnabled = true } = options
|
|
198
213
|
switch (data.type) {
|
|
199
214
|
case MESSAGE_TYPE.TEXT:
|
|
200
215
|
if ('text' in data && data.text) {
|
|
@@ -217,6 +232,7 @@ function processMessageData(
|
|
|
217
232
|
integratedToolType: data.integratedToolType || '',
|
|
218
233
|
toolFunction: data.toolFunction || '',
|
|
219
234
|
parameters: data.parameters,
|
|
235
|
+
toolExecutionRequestId: data.toolExecutionRequestId,
|
|
220
236
|
},
|
|
221
237
|
})
|
|
222
238
|
}
|
|
@@ -233,6 +249,7 @@ function processMessageData(
|
|
|
233
249
|
parameters: data.parameters,
|
|
234
250
|
result: data.result,
|
|
235
251
|
success: data.success,
|
|
252
|
+
toolExecutionRequestId: data.toolExecutionRequestId,
|
|
236
253
|
},
|
|
237
254
|
})
|
|
238
255
|
}
|
|
@@ -241,18 +258,42 @@ function processMessageData(
|
|
|
241
258
|
case MESSAGE_TYPE.APPROVAL_REQUEST:
|
|
242
259
|
if ('approvalRequestId' in data && data.approvalRequestId) {
|
|
243
260
|
const approvalType = data.approvalType || 'CLIENT'
|
|
261
|
+
const toolCalls: PendingToolCallData[] | undefined = Array.isArray(data.toolCalls)
|
|
262
|
+
? data.toolCalls
|
|
263
|
+
: undefined
|
|
264
|
+
const isBatch = !!toolCalls && toolCalls.length > 0
|
|
244
265
|
|
|
245
266
|
if (!displayApprovalTypes || displayApprovalTypes.includes(approvalType)) {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
267
|
+
if (isBatch) {
|
|
268
|
+
const status = (approvalStatuses[data.approvalRequestId] as ChatApprovalStatus) || 'pending'
|
|
269
|
+
if (batchApprovalsEnabled) {
|
|
270
|
+
accumulator.addApprovalBatch(data.approvalRequestId, approvalType, toolCalls!, status)
|
|
271
|
+
} else {
|
|
272
|
+
// Flag OFF — unfold batch into N legacy approval cards (same id).
|
|
273
|
+
for (const call of toolCalls!) {
|
|
274
|
+
if (!call.requiresApproval) continue
|
|
275
|
+
accumulator.addApprovalRequest(
|
|
276
|
+
data.approvalRequestId,
|
|
277
|
+
getCommandText(call),
|
|
278
|
+
call.toolExplanation,
|
|
279
|
+
approvalType,
|
|
280
|
+
status,
|
|
281
|
+
)
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
} else {
|
|
285
|
+
accumulator.trackApprovalRequest(data.approvalRequestId, {
|
|
286
|
+
command: data.command || '',
|
|
287
|
+
explanation: data.explanation,
|
|
288
|
+
approvalType,
|
|
289
|
+
})
|
|
290
|
+
}
|
|
251
291
|
} else {
|
|
252
292
|
escalatedApprovals?.set(data.approvalRequestId, {
|
|
253
293
|
command: data.command || '',
|
|
254
294
|
explanation: data.explanation,
|
|
255
295
|
approvalType,
|
|
296
|
+
...(isBatch ? { toolCalls } : {}),
|
|
256
297
|
})
|
|
257
298
|
}
|
|
258
299
|
}
|
|
@@ -260,10 +301,34 @@ function processMessageData(
|
|
|
260
301
|
|
|
261
302
|
case MESSAGE_TYPE.APPROVAL_RESULT:
|
|
262
303
|
if ('approvalRequestId' in data && data.approvalRequestId) {
|
|
263
|
-
const existingStatus = approvalStatuses[data.approvalRequestId]
|
|
264
|
-
const status = existingStatus || (data.approved ? 'approved' : 'rejected')
|
|
304
|
+
const existingStatus = approvalStatuses[data.approvalRequestId] as ChatApprovalStatus | undefined
|
|
305
|
+
const status: ChatApprovalStatus = existingStatus || (data.approved ? 'approved' : 'rejected')
|
|
265
306
|
const escalatedData = escalatedApprovals?.get(data.approvalRequestId)
|
|
266
307
|
|
|
308
|
+
if (escalatedData?.toolCalls && escalatedData.toolCalls.length > 0) {
|
|
309
|
+
if (batchApprovalsEnabled) {
|
|
310
|
+
accumulator.addApprovalBatch(
|
|
311
|
+
data.approvalRequestId,
|
|
312
|
+
escalatedData.approvalType,
|
|
313
|
+
escalatedData.toolCalls,
|
|
314
|
+
status,
|
|
315
|
+
)
|
|
316
|
+
} else {
|
|
317
|
+
for (const call of escalatedData.toolCalls) {
|
|
318
|
+
if (!call.requiresApproval) continue
|
|
319
|
+
accumulator.addApprovalRequest(
|
|
320
|
+
data.approvalRequestId,
|
|
321
|
+
getCommandText(call),
|
|
322
|
+
call.toolExplanation,
|
|
323
|
+
escalatedData.approvalType,
|
|
324
|
+
status,
|
|
325
|
+
)
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
escalatedApprovals?.delete(data.approvalRequestId)
|
|
329
|
+
break
|
|
330
|
+
}
|
|
331
|
+
|
|
267
332
|
if (escalatedData) {
|
|
268
333
|
accumulator.trackApprovalRequest(data.approvalRequestId, {
|
|
269
334
|
command: escalatedData.command,
|
|
@@ -272,7 +337,14 @@ function processMessageData(
|
|
|
272
337
|
})
|
|
273
338
|
escalatedApprovals?.delete(data.approvalRequestId)
|
|
274
339
|
}
|
|
275
|
-
|
|
340
|
+
|
|
341
|
+
// If a segment with this id is already present (batch or legacy), just flip its status.
|
|
342
|
+
// updateApprovalStatus matches both `approval_batch` and `approval_request` segments.
|
|
343
|
+
const before = accumulator.getSegments()
|
|
344
|
+
const after = accumulator.updateApprovalStatus(data.approvalRequestId, status)
|
|
345
|
+
const updatedExisting = before.some((s, i) => after[i] !== s)
|
|
346
|
+
if (updatedExisting) break
|
|
347
|
+
|
|
276
348
|
accumulator.processApprovalResult(
|
|
277
349
|
data.approvalRequestId,
|
|
278
350
|
status === 'approved',
|
|
@@ -364,7 +436,10 @@ export function processHistoricalMessagesWithErrors(
|
|
|
364
436
|
|
|
365
437
|
const processedMessages: ProcessedMessage[] = []
|
|
366
438
|
const accumulator = createMessageSegmentAccumulator({ onApprove, onReject })
|
|
367
|
-
const escalatedApprovals = new Map<
|
|
439
|
+
const escalatedApprovals = new Map<
|
|
440
|
+
string,
|
|
441
|
+
{ command: string; explanation?: string; approvalType: string; toolCalls?: PendingToolCallData[] }
|
|
442
|
+
>()
|
|
368
443
|
|
|
369
444
|
let currentAssistantId: string | null = null
|
|
370
445
|
let currentAssistantTimestamp: Date | null = null
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { PendingToolCallData } from '../types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Keys in `toolCallArguments` whose value is the actual command/query/script
|
|
5
|
+
* body — anything that should be rendered as the tool's "title" line in the
|
|
6
|
+
* approval card. Order matters: first match wins.
|
|
7
|
+
*/
|
|
8
|
+
export const COMMAND_BODY_ARG_KEYS = ['command', 'query', 'script', 'scriptContent', 'code'] as const
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Extract a human-readable command string from a tool call.
|
|
12
|
+
* Order: command/query/script/scriptContent/code arg → toolTitle → toolName → fallback.
|
|
13
|
+
*/
|
|
14
|
+
export function getCommandText(call: PendingToolCallData): string {
|
|
15
|
+
const args = call.toolCallArguments
|
|
16
|
+
if (args && typeof args === 'object') {
|
|
17
|
+
const a = args as Record<string, unknown>
|
|
18
|
+
for (const key of COMMAND_BODY_ARG_KEYS) {
|
|
19
|
+
const candidate = a[key]
|
|
20
|
+
if (typeof candidate === 'string' && candidate.trim()) return candidate
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return call.toolTitle || call.toolName || 'Tool call'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type FormattedArgValue =
|
|
27
|
+
| { kind: 'inline'; text: string }
|
|
28
|
+
| { kind: 'block'; text: string; language: 'json' | 'text' }
|
|
29
|
+
|
|
30
|
+
const INLINE_STRING_MAX = 80
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Decide how to render a single `toolCallArguments` value:
|
|
34
|
+
* - objects/arrays → pretty-printed JSON block
|
|
35
|
+
* - strings that parse as JSON → pretty-printed JSON block
|
|
36
|
+
* - multi-line or long strings → preserved-whitespace text block
|
|
37
|
+
* - everything else → inline single line
|
|
38
|
+
*/
|
|
39
|
+
export function formatToolArgValue(value: unknown): FormattedArgValue {
|
|
40
|
+
if (value === null || value === undefined) return { kind: 'inline', text: '' }
|
|
41
|
+
|
|
42
|
+
if (typeof value === 'object') {
|
|
43
|
+
return { kind: 'block', text: safeStringify(value), language: 'json' }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (typeof value === 'string') {
|
|
47
|
+
const trimmed = value.trim()
|
|
48
|
+
if (
|
|
49
|
+
(trimmed.startsWith('{') && trimmed.endsWith('}')) ||
|
|
50
|
+
(trimmed.startsWith('[') && trimmed.endsWith(']'))
|
|
51
|
+
) {
|
|
52
|
+
try {
|
|
53
|
+
return { kind: 'block', text: JSON.stringify(JSON.parse(trimmed), null, 2), language: 'json' }
|
|
54
|
+
} catch {
|
|
55
|
+
// not valid JSON — fall through
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (value.includes('\n') || value.length > INLINE_STRING_MAX) {
|
|
59
|
+
return { kind: 'block', text: value, language: 'text' }
|
|
60
|
+
}
|
|
61
|
+
return { kind: 'inline', text: value }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return { kind: 'inline', text: String(value) }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function safeStringify(value: unknown): string {
|
|
68
|
+
try {
|
|
69
|
+
return JSON.stringify(value, null, 2)
|
|
70
|
+
} catch {
|
|
71
|
+
return String(value)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Decide how to render a tool execution `result` string.
|
|
77
|
+
* - Strips a single surrounding markdown code fence (``` … ```), since the
|
|
78
|
+
* backend often wraps Bash/PowerShell output that way.
|
|
79
|
+
* - If the body parses as JSON, returns a pretty-printed JSON block.
|
|
80
|
+
* - If the body is multi-line / long, returns a preserved-whitespace block.
|
|
81
|
+
* - Otherwise inline.
|
|
82
|
+
*/
|
|
83
|
+
export function formatToolResult(value: string | undefined | null): FormattedArgValue {
|
|
84
|
+
if (typeof value !== 'string') return { kind: 'inline', text: '' }
|
|
85
|
+
|
|
86
|
+
const fenced = value.match(/^\s*```(?:[a-zA-Z0-9_-]*)\n([\s\S]*?)\n```\s*$/)
|
|
87
|
+
const body = fenced ? fenced[1] : value
|
|
88
|
+
const trimmed = body.trim()
|
|
89
|
+
|
|
90
|
+
if (
|
|
91
|
+
(trimmed.startsWith('{') && trimmed.endsWith('}')) ||
|
|
92
|
+
(trimmed.startsWith('[') && trimmed.endsWith(']'))
|
|
93
|
+
) {
|
|
94
|
+
try {
|
|
95
|
+
return { kind: 'block', text: JSON.stringify(JSON.parse(trimmed), null, 2), language: 'json' }
|
|
96
|
+
} catch {
|
|
97
|
+
// fall through
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (body.includes('\n') || body.length > INLINE_STRING_MAX) {
|
|
102
|
+
return { kind: 'block', text: body, language: 'text' }
|
|
103
|
+
}
|
|
104
|
+
return { kind: 'inline', text: body }
|
|
105
|
+
}
|
|
@@ -12,40 +12,122 @@ export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextArea
|
|
|
12
12
|
label?: string;
|
|
13
13
|
/** Error message displayed below the textarea */
|
|
14
14
|
error?: string;
|
|
15
|
+
/** Element rendered at the right edge of the field (e.g. send icon). */
|
|
16
|
+
endIcon?: React.ReactNode;
|
|
17
|
+
/**
|
|
18
|
+
* When true, `endIcon` is wrapped in a real `<button>` with hover / active
|
|
19
|
+
* / focus-visible styling matching the rest of the design system. Pass any
|
|
20
|
+
* extra button attributes (`onClick`, `disabled`, `aria-label`, …) via
|
|
21
|
+
* `endIconButtonProps`.
|
|
22
|
+
*/
|
|
23
|
+
endIconAsButton?: boolean;
|
|
24
|
+
/** Extra attributes for the end-icon button. */
|
|
25
|
+
endIconButtonProps?: Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'children'>;
|
|
15
26
|
}
|
|
16
27
|
|
|
17
28
|
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|
18
|
-
({ className, invalid = false, label, error, ...props }, ref) => {
|
|
29
|
+
({ className, invalid = false, label, error, endIcon, endIconAsButton = false, endIconButtonProps, ...props }, ref) => {
|
|
19
30
|
const isInvalid = invalid || !!error
|
|
31
|
+
const hasEndIcon = !!endIcon
|
|
20
32
|
|
|
33
|
+
// Without an end icon we keep the historical layout: bare textarea, the
|
|
34
|
+
// border/bg/hover all live on the textarea itself.
|
|
35
|
+
if (!hasEndIcon) {
|
|
36
|
+
return (
|
|
37
|
+
<FieldWrapper label={label} error={error}>
|
|
38
|
+
<textarea
|
|
39
|
+
className={cn(
|
|
40
|
+
"flex min-h-[96px] w-full rounded-[6px] border p-3",
|
|
41
|
+
"text-h4",
|
|
42
|
+
"focus-visible:outline-none focus:border-ods-accent",
|
|
43
|
+
"transition-colors duration-200 touch-manipulation",
|
|
44
|
+
"bg-ods-card border-ods-border text-ods-text-primary placeholder:text-ods-text-secondary",
|
|
45
|
+
!props.disabled && "hover:bg-ods-bg-hover hover:border-ods-border-hover active:bg-ods-bg-active active:border-ods-border-active",
|
|
46
|
+
props.disabled && "!cursor-not-allowed bg-ods-bg disabled:placeholder:text-ods-border",
|
|
47
|
+
"cursor-text relative z-10",
|
|
48
|
+
"resize-y",
|
|
49
|
+
isInvalid && "border-ods-error hover:border-ods-error focus:border-ods-error",
|
|
50
|
+
className,
|
|
51
|
+
)}
|
|
52
|
+
ref={ref}
|
|
53
|
+
{...props}
|
|
54
|
+
/>
|
|
55
|
+
</FieldWrapper>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// With an end icon we lift border/bg onto a `<label>` wrapper so the icon
|
|
60
|
+
// sits visually inside the same field. The textarea becomes a transparent
|
|
61
|
+
// child. Wrapper classes mirror `Input` 1:1 (same items-center, same
|
|
62
|
+
// `has-[:focus]` selectors, same active state, same outline reset) so
|
|
63
|
+
// hover / focus / cursor behaviour matches the standard input exactly.
|
|
21
64
|
const content = (
|
|
22
|
-
<
|
|
65
|
+
<label
|
|
66
|
+
data-invalid={isInvalid || undefined}
|
|
23
67
|
className={cn(
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
"focus-visible:outline-none
|
|
30
|
-
|
|
31
|
-
"transition-colors duration-200
|
|
32
|
-
|
|
33
|
-
"bg-ods-card border-ods-border text-ods-text-primary placeholder:text-ods-text-secondary",
|
|
34
|
-
// Hover & active (not disabled)
|
|
68
|
+
// Wrapper mirrors `Input`, but uses `items-end` so the icon sticks
|
|
69
|
+
// to the bottom-right when the textarea grows multi-line. Vertical
|
|
70
|
+
// padding lives here (not on the textarea) so both children share
|
|
71
|
+
// the same bottom baseline at any height.
|
|
72
|
+
"flex w-full items-end gap-2 rounded-[6px] border px-3 py-2.5 md:py-3 min-h-11 md:min-h-12 cursor-text",
|
|
73
|
+
"has-[:focus-visible]:outline-none",
|
|
74
|
+
"group",
|
|
75
|
+
"transition-colors duration-200",
|
|
76
|
+
"bg-ods-card border-ods-border has-[:focus]:border-ods-accent",
|
|
35
77
|
!props.disabled && "hover:bg-ods-bg-hover hover:border-ods-border-hover active:bg-ods-bg-active active:border-ods-border-active",
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
78
|
+
props.disabled && "!cursor-not-allowed bg-ods-bg",
|
|
79
|
+
isInvalid && "border-ods-error hover:border-ods-error has-[:focus]:border-ods-error",
|
|
80
|
+
)}
|
|
81
|
+
>
|
|
82
|
+
<textarea
|
|
83
|
+
ref={ref}
|
|
84
|
+
rows={1}
|
|
85
|
+
className={cn(
|
|
86
|
+
"flex-1 min-w-0 resize-none bg-transparent border-none outline-none p-0 m-0 box-border",
|
|
87
|
+
// Native CSS auto-grow (Chrome 123+, FF 124+, Safari 16.4+) — no
|
|
88
|
+
// JS needed to size to content. Falls back to `rows={1}` natural
|
|
89
|
+
// height on older browsers.
|
|
90
|
+
"[field-sizing:content]",
|
|
91
|
+
"text-h4 text-ods-text-primary placeholder:text-ods-text-secondary leading-6",
|
|
92
|
+
// Hard cap on growth: beyond `max-h` the textarea scrolls
|
|
93
|
+
// internally instead of pushing the icon out of frame.
|
|
94
|
+
"max-h-[160px] overflow-y-auto",
|
|
95
|
+
"scrollbar-thin scrollbar-track-transparent scrollbar-thumb-ods-border/30 hover:scrollbar-thumb-ods-text-secondary/30",
|
|
96
|
+
"disabled:cursor-not-allowed disabled:placeholder:text-ods-border",
|
|
97
|
+
className,
|
|
98
|
+
)}
|
|
99
|
+
{...props}
|
|
100
|
+
/>
|
|
101
|
+
|
|
102
|
+
{endIconAsButton ? (
|
|
103
|
+
<button
|
|
104
|
+
type="button"
|
|
105
|
+
aria-label={endIconButtonProps?.['aria-label'] ?? 'Submit'}
|
|
106
|
+
{...endIconButtonProps}
|
|
107
|
+
className={cn(
|
|
108
|
+
"flex shrink-0 items-center text-ods-text-secondary transition-colors duration-200",
|
|
109
|
+
"group-has-[:focus]:text-ods-accent group-data-[invalid]:text-ods-error",
|
|
110
|
+
"[&_svg]:size-4 md:[&_svg]:size-6",
|
|
111
|
+
"cursor-pointer hover:text-ods-text-primary",
|
|
112
|
+
"focus-visible:outline-none focus-visible:text-ods-accent",
|
|
113
|
+
"disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:text-ods-text-secondary",
|
|
114
|
+
endIconButtonProps?.className,
|
|
115
|
+
)}
|
|
116
|
+
>
|
|
117
|
+
{endIcon}
|
|
118
|
+
</button>
|
|
119
|
+
) : (
|
|
120
|
+
<span
|
|
121
|
+
className={cn(
|
|
122
|
+
"flex shrink-0 items-center text-ods-text-secondary transition-colors duration-200",
|
|
123
|
+
"group-has-[:focus]:text-ods-accent group-data-[invalid]:text-ods-error",
|
|
124
|
+
"[&_svg]:size-4 md:[&_svg]:size-6",
|
|
125
|
+
)}
|
|
126
|
+
>
|
|
127
|
+
{endIcon}
|
|
128
|
+
</span>
|
|
45
129
|
)}
|
|
46
|
-
|
|
47
|
-
{...props}
|
|
48
|
-
/>
|
|
130
|
+
</label>
|
|
49
131
|
)
|
|
50
132
|
|
|
51
133
|
return (
|