@adea-ai/ui 0.68.2 → 0.69.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/README.md +79 -15
- package/dist/NOTICE +55 -24
- package/dist/components/conversation/atomic/chat-composer.d.ts +20 -0
- package/dist/components/conversation/atomic/chat-composer.d.ts.map +1 -0
- package/dist/components/conversation/atomic/chat-composer.js +69 -0
- package/dist/components/conversation/atomic/chat-composer.js.map +1 -0
- package/dist/components/conversation/atomic/index.d.ts +3 -0
- package/dist/components/conversation/atomic/index.d.ts.map +1 -0
- package/dist/components/conversation/atomic/index.js +2 -0
- package/dist/components/conversation/chat-composer.d.ts +12 -21
- package/dist/components/conversation/chat-composer.d.ts.map +1 -1
- package/dist/components/conversation/chat-composer.js +33 -403
- package/dist/components/conversation/chat-composer.js.map +1 -1
- package/dist/components/conversation/internal/chat-composer-shell.d.ts +38 -0
- package/dist/components/conversation/internal/chat-composer-shell.d.ts.map +1 -0
- package/dist/components/conversation/internal/chat-composer-shell.js +423 -0
- package/dist/components/conversation/internal/chat-composer-shell.js.map +1 -0
- package/dist/components/conversation/paste-preview.d.ts +11 -0
- package/dist/components/conversation/paste-preview.d.ts.map +1 -0
- package/dist/components/conversation/paste-preview.js +28 -0
- package/dist/components/conversation/paste-preview.js.map +1 -0
- package/dist/components/conversation/paste-token-editor.d.ts +26 -0
- package/dist/components/conversation/paste-token-editor.d.ts.map +1 -0
- package/dist/components/conversation/paste-token-editor.js +570 -0
- package/dist/components/conversation/paste-token-editor.js.map +1 -0
- package/dist/components/conversation/paste-tokens.d.ts.map +1 -1
- package/dist/components/conversation/paste-tokens.js +11 -2
- package/dist/components/conversation/paste-tokens.js.map +1 -1
- package/dist/r/conversation.json +26 -0
- package/dist/r/registry.json +26 -0
- package/dist/r/src/components/conversation/atomic/chat-composer.tsx +57 -0
- package/dist/r/src/components/conversation/atomic/index.ts +6 -0
- package/dist/r/src/components/conversation/chat-composer.tsx +39 -331
- package/dist/r/src/components/conversation/internal/chat-composer-shell.tsx +390 -0
- package/dist/r/src/components/conversation/paste-preview.ts +35 -0
- package/dist/r/src/components/conversation/paste-token-editor.tsx +672 -0
- package/dist/r/src/components/conversation/paste-tokens.ts +15 -2
- package/package.json +1 -1
- package/registry.json +26 -0
- package/src/components/conversation/atomic/chat-composer.tsx +57 -0
- package/src/components/conversation/atomic/index.ts +6 -0
- package/src/components/conversation/chat-composer.tsx +39 -331
- package/src/components/conversation/internal/chat-composer-shell.tsx +390 -0
- package/src/components/conversation/paste-preview.ts +35 -0
- package/src/components/conversation/paste-token-editor.tsx +672 -0
- package/src/components/conversation/paste-tokens.ts +15 -2
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Substantially translated from KiroCrew website/src/components/ChatInput.tsx
|
|
3
|
+
* at 283e136c0f902e965a535a7c9548c57c7504fed0.
|
|
4
|
+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
5
|
+
* Licensed under Apache-2.0; see LICENSE and NOTICE.
|
|
6
|
+
*/
|
|
7
|
+
import { ChevronsDownUp, ChevronsUpDown, MoreHorizontal, ArrowUp } from 'lucide-solid'
|
|
8
|
+
import {
|
|
9
|
+
Show,
|
|
10
|
+
createEffect,
|
|
11
|
+
createSignal,
|
|
12
|
+
on,
|
|
13
|
+
onCleanup,
|
|
14
|
+
onMount,
|
|
15
|
+
type Accessor,
|
|
16
|
+
type JSX,
|
|
17
|
+
} from 'solid-js'
|
|
18
|
+
import type {
|
|
19
|
+
ChatComposerControl,
|
|
20
|
+
ChatComposerSharedProps,
|
|
21
|
+
ChatComposerSubmitInput,
|
|
22
|
+
} from '../chat-composer'
|
|
23
|
+
import { Button } from '../../ui/button/button'
|
|
24
|
+
import {
|
|
25
|
+
DropdownMenu,
|
|
26
|
+
DropdownMenuContent,
|
|
27
|
+
DropdownMenuItem,
|
|
28
|
+
DropdownMenuTrigger,
|
|
29
|
+
} from '../../ui/dropdown-menu/dropdown-menu'
|
|
30
|
+
import { Spinner } from '../../ui/spinner/spinner'
|
|
31
|
+
import { BusySendButton, type BusySendMode } from '../busy-send-button'
|
|
32
|
+
import { createComposerImeGuard } from '../ime-guard'
|
|
33
|
+
|
|
34
|
+
/** Private typed seam: public wrappers supply the native or atomic input. */
|
|
35
|
+
type ChatComposerInputRenderProps = {
|
|
36
|
+
ref: (element: HTMLTextAreaElement | undefined) => void
|
|
37
|
+
value: Accessor<string>
|
|
38
|
+
resetKey: Accessor<unknown>
|
|
39
|
+
inputLabel: Accessor<string>
|
|
40
|
+
placeholder: Accessor<string>
|
|
41
|
+
disabled: Accessor<boolean | undefined>
|
|
42
|
+
readOnly: Accessor<boolean>
|
|
43
|
+
composing: Accessor<boolean>
|
|
44
|
+
onCompositionStart: () => void
|
|
45
|
+
onCompositionEnd: () => void
|
|
46
|
+
onFocus: () => void
|
|
47
|
+
onBlur: () => void
|
|
48
|
+
onKeyDown: (event: KeyboardEvent) => void
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type ChatComposerShellProps<Extra extends object> = ChatComposerSharedProps & {
|
|
52
|
+
createSubmitExtra: (input: ChatComposerSubmitInput) => Extra
|
|
53
|
+
onSubmit: (input: ChatComposerSubmitInput & Extra) => void | Promise<void>
|
|
54
|
+
renderInput: (props: ChatComposerInputRenderProps) => JSX.Element
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Kiro's composer assembly: context/options, adjacent band, approval/notices,
|
|
59
|
+
* staged content, input/action groups and a context shelf. Reading collapse
|
|
60
|
+
* really unmounts the input and shelf and offers a labeled way back with draft
|
|
61
|
+
* preview. The host retains the only draft, identities, preferences and services.
|
|
62
|
+
*
|
|
63
|
+
* Kobalte owns the options menu. Scoped imperative typing intent replaces the
|
|
64
|
+
* donor global lookup/broadcast so a second composer cannot receive this draft
|
|
65
|
+
* or focus. No animation starts from hidden geometry, no global shortcut, and no
|
|
66
|
+
* runtime/permission/voice/optimizer authority is copied. More specific chips,
|
|
67
|
+
* approval decisions and staged-content semantics are supplied by their owners.
|
|
68
|
+
*/
|
|
69
|
+
export function ChatComposerShell<Extra extends object>(props: ChatComposerShellProps<Extra>) {
|
|
70
|
+
const ime = createComposerImeGuard()
|
|
71
|
+
const [pending, setPending] = createSignal(false)
|
|
72
|
+
const [error, setError] = createSignal<string | null>(null)
|
|
73
|
+
let field: HTMLTextAreaElement | undefined
|
|
74
|
+
let restoreBar: HTMLButtonElement | undefined
|
|
75
|
+
let focusFrame: number | undefined
|
|
76
|
+
let focusTarget: 'input' | 'bar' | undefined
|
|
77
|
+
let selection:
|
|
78
|
+
| { start: number; end: number; direction: 'forward' | 'backward' | 'none' }
|
|
79
|
+
| undefined
|
|
80
|
+
let generation = 0
|
|
81
|
+
let active = true
|
|
82
|
+
let composing = false
|
|
83
|
+
const collapsed = () => props.collapse?.value === true
|
|
84
|
+
const unavailable = () => props.disabled || props.readOnly || !!props.blockedReason
|
|
85
|
+
const cancelFocus = () => {
|
|
86
|
+
if (focusFrame !== undefined) cancelAnimationFrame(focusFrame)
|
|
87
|
+
focusFrame = undefined
|
|
88
|
+
}
|
|
89
|
+
const requestFocus = (target: 'input' | 'bar') => {
|
|
90
|
+
cancelFocus()
|
|
91
|
+
focusTarget = target
|
|
92
|
+
focusFrame = requestAnimationFrame(() => {
|
|
93
|
+
focusFrame = undefined
|
|
94
|
+
if (!active || props.approvalFocus) return
|
|
95
|
+
if (target === 'bar' && collapsed()) restoreBar?.focus()
|
|
96
|
+
if (target === 'input' && !collapsed() && !props.disabled && field) {
|
|
97
|
+
field.focus()
|
|
98
|
+
if (selection)
|
|
99
|
+
field.setSelectionRange(
|
|
100
|
+
Math.min(selection.start, field.value.length),
|
|
101
|
+
Math.min(selection.end, field.value.length),
|
|
102
|
+
selection.direction
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
focusTarget = undefined
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
const changeCollapsed = (value: boolean) => {
|
|
109
|
+
if (!props.collapse || props.approvalFocus) return
|
|
110
|
+
if (value && field)
|
|
111
|
+
selection = {
|
|
112
|
+
start: field.selectionStart,
|
|
113
|
+
end: field.selectionEnd,
|
|
114
|
+
direction: field.selectionDirection,
|
|
115
|
+
}
|
|
116
|
+
props.collapse.onChange(value)
|
|
117
|
+
requestFocus(value ? 'bar' : 'input')
|
|
118
|
+
}
|
|
119
|
+
const control: ChatComposerControl = {
|
|
120
|
+
focus() {
|
|
121
|
+
if (collapsed() || props.approvalFocus || props.disabled || !field) return false
|
|
122
|
+
field.focus()
|
|
123
|
+
return true
|
|
124
|
+
},
|
|
125
|
+
expandAndFocus() {
|
|
126
|
+
if (props.approvalFocus || props.disabled) return false
|
|
127
|
+
if (collapsed()) changeCollapsed(false)
|
|
128
|
+
else requestFocus('input')
|
|
129
|
+
return true
|
|
130
|
+
},
|
|
131
|
+
}
|
|
132
|
+
onMount(() => props.controlRef?.(control))
|
|
133
|
+
createEffect(
|
|
134
|
+
on(
|
|
135
|
+
() => props.resetKey,
|
|
136
|
+
() => {
|
|
137
|
+
generation += 1
|
|
138
|
+
selection = undefined
|
|
139
|
+
composing = false
|
|
140
|
+
cancelFocus()
|
|
141
|
+
focusTarget = undefined
|
|
142
|
+
setPending(false)
|
|
143
|
+
setError(null)
|
|
144
|
+
ime.onBlur()
|
|
145
|
+
},
|
|
146
|
+
{ defer: true }
|
|
147
|
+
)
|
|
148
|
+
)
|
|
149
|
+
onCleanup(() => {
|
|
150
|
+
active = false
|
|
151
|
+
generation += 1
|
|
152
|
+
cancelFocus()
|
|
153
|
+
props.controlRef?.(undefined)
|
|
154
|
+
})
|
|
155
|
+
const action = (): 'send' | BusySendMode => props.busy?.mode ?? 'send'
|
|
156
|
+
const canSend = (selected: 'send' | BusySendMode = action()) =>
|
|
157
|
+
!unavailable() &&
|
|
158
|
+
!pending() &&
|
|
159
|
+
(props.sendableActions?.[selected] ?? props.value.trim().length > 0) &&
|
|
160
|
+
!(selected !== 'send' && props.busy?.unavailable?.[selected])
|
|
161
|
+
const submit = async (selected: 'send' | BusySendMode = action()) => {
|
|
162
|
+
if (!canSend(selected)) return
|
|
163
|
+
const started = generation
|
|
164
|
+
const base: ChatComposerSubmitInput = { text: props.value, action: selected }
|
|
165
|
+
const input = { ...base, ...props.createSubmitExtra(base) }
|
|
166
|
+
setPending(true)
|
|
167
|
+
setError(null)
|
|
168
|
+
try {
|
|
169
|
+
await props.onSubmit(input)
|
|
170
|
+
} catch {
|
|
171
|
+
if (active && started === generation)
|
|
172
|
+
setError('Message not sent. Check your draft and retry when the connection recovers.')
|
|
173
|
+
} finally {
|
|
174
|
+
if (active && started === generation) setPending(false)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const preview = () => {
|
|
178
|
+
const first =
|
|
179
|
+
props.value
|
|
180
|
+
.split('\n')
|
|
181
|
+
.find((line) => line.trim().length > 0)
|
|
182
|
+
?.trim() ?? ''
|
|
183
|
+
return first.length > 120 ? `${first.slice(0, 120)}…` : first
|
|
184
|
+
}
|
|
185
|
+
const handleInputKeyDown = (event: KeyboardEvent) => {
|
|
186
|
+
if (event.key === 'Escape' && error()) {
|
|
187
|
+
setError(null)
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
if (event.key !== 'Enter' || event.shiftKey || event.defaultPrevented) return
|
|
191
|
+
if (ime.claimEnter(event)) {
|
|
192
|
+
const selected =
|
|
193
|
+
props.busy && (event.metaKey || event.ctrlKey)
|
|
194
|
+
? props.busy.mode === 'steer'
|
|
195
|
+
? 'queue'
|
|
196
|
+
: 'steer'
|
|
197
|
+
: action()
|
|
198
|
+
void submit(selected)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const handleCompositionStart = () => {
|
|
202
|
+
composing = true
|
|
203
|
+
ime.onCompositionStart()
|
|
204
|
+
}
|
|
205
|
+
const handleCompositionEnd = () => {
|
|
206
|
+
composing = false
|
|
207
|
+
ime.onCompositionEnd()
|
|
208
|
+
}
|
|
209
|
+
const handleBlur = () => {
|
|
210
|
+
composing = false
|
|
211
|
+
ime.onBlur()
|
|
212
|
+
}
|
|
213
|
+
return (
|
|
214
|
+
<div data-slot="chat-composer" class="mx-auto flex w-full max-w-4xl flex-col px-4 pb-1">
|
|
215
|
+
<Show when={!props.approvalFocus}>
|
|
216
|
+
<Show when={props.knowledge}>
|
|
217
|
+
<div data-slot="composer-knowledge">{props.knowledge}</div>
|
|
218
|
+
</Show>
|
|
219
|
+
<Show when={props.followUps}>
|
|
220
|
+
<div data-slot="composer-follow-ups">{props.followUps}</div>
|
|
221
|
+
</Show>
|
|
222
|
+
</Show>
|
|
223
|
+
<Show when={props.band}>
|
|
224
|
+
<div data-slot="composer-band">{props.band}</div>
|
|
225
|
+
</Show>
|
|
226
|
+
<div aria-hidden="true" class="h-1.5 shrink-0" />
|
|
227
|
+
<Show when={props.approval}>
|
|
228
|
+
<div data-slot="composer-approval">{props.approval}</div>
|
|
229
|
+
</Show>
|
|
230
|
+
<Show when={props.notices}>
|
|
231
|
+
<div data-slot="composer-notices">{props.notices}</div>
|
|
232
|
+
</Show>
|
|
233
|
+
<Show when={props.blockedReason}>
|
|
234
|
+
<p role="status" class="text-muted-foreground mb-1 text-xs">
|
|
235
|
+
{props.blockedReason}
|
|
236
|
+
</p>
|
|
237
|
+
</Show>
|
|
238
|
+
<Show when={error()}>
|
|
239
|
+
{(message) => (
|
|
240
|
+
<p role="alert" class="text-destructive mb-1 text-xs">
|
|
241
|
+
{message()}
|
|
242
|
+
</p>
|
|
243
|
+
)}
|
|
244
|
+
</Show>
|
|
245
|
+
<Show when={!props.approvalFocus}>
|
|
246
|
+
<Show
|
|
247
|
+
when={!collapsed()}
|
|
248
|
+
fallback={
|
|
249
|
+
<Button
|
|
250
|
+
ref={(element) => {
|
|
251
|
+
restoreBar = element
|
|
252
|
+
onCleanup(() => {
|
|
253
|
+
if (restoreBar === element) restoreBar = undefined
|
|
254
|
+
})
|
|
255
|
+
}}
|
|
256
|
+
type="button"
|
|
257
|
+
variant="outline"
|
|
258
|
+
size="sm"
|
|
259
|
+
class="w-full justify-start"
|
|
260
|
+
aria-expanded={false}
|
|
261
|
+
aria-label="Show the message input"
|
|
262
|
+
onClick={() => changeCollapsed(false)}
|
|
263
|
+
>
|
|
264
|
+
<ChevronsUpDown />
|
|
265
|
+
<span aria-hidden="true" class="shrink-0">
|
|
266
|
+
Show the message input
|
|
267
|
+
</span>
|
|
268
|
+
<Show when={preview()}>
|
|
269
|
+
<span aria-hidden="true" class="min-w-0 flex-1 truncate">
|
|
270
|
+
{preview()}
|
|
271
|
+
</span>
|
|
272
|
+
</Show>
|
|
273
|
+
</Button>
|
|
274
|
+
}
|
|
275
|
+
>
|
|
276
|
+
<form
|
|
277
|
+
data-slot="composer-input-box"
|
|
278
|
+
class="border-input bg-card flex flex-col overflow-hidden rounded-2xl border transition-colors focus-within:border-ring"
|
|
279
|
+
onSubmit={(event) => {
|
|
280
|
+
event.preventDefault()
|
|
281
|
+
void submit()
|
|
282
|
+
}}
|
|
283
|
+
>
|
|
284
|
+
<Show when={props.attachments}>
|
|
285
|
+
<div data-slot="composer-staged-content">{props.attachments}</div>
|
|
286
|
+
</Show>
|
|
287
|
+
{props.renderInput({
|
|
288
|
+
ref: (element) => {
|
|
289
|
+
field = element
|
|
290
|
+
if (element)
|
|
291
|
+
onCleanup(() => {
|
|
292
|
+
if (field === element) field = undefined
|
|
293
|
+
})
|
|
294
|
+
},
|
|
295
|
+
value: () => props.value,
|
|
296
|
+
resetKey: () => props.resetKey,
|
|
297
|
+
inputLabel: () => props.inputLabel ?? 'Message',
|
|
298
|
+
placeholder: () => props.placeholder ?? 'Write a message…',
|
|
299
|
+
disabled: () => props.disabled,
|
|
300
|
+
readOnly: () => props.readOnly || pending(),
|
|
301
|
+
composing: () => composing,
|
|
302
|
+
onCompositionStart: handleCompositionStart,
|
|
303
|
+
onCompositionEnd: handleCompositionEnd,
|
|
304
|
+
onFocus: ime.onFocus,
|
|
305
|
+
onBlur: handleBlur,
|
|
306
|
+
onKeyDown: handleInputKeyDown,
|
|
307
|
+
})}
|
|
308
|
+
<div
|
|
309
|
+
data-slot="composer-action-row"
|
|
310
|
+
class="flex items-center justify-between gap-2 px-2.5 pb-2 pt-0.5"
|
|
311
|
+
>
|
|
312
|
+
<div class="flex min-w-0 items-center gap-1">
|
|
313
|
+
<Show when={props.collapse || props.menuItems}>
|
|
314
|
+
<DropdownMenu placement="top-start">
|
|
315
|
+
<DropdownMenuTrigger
|
|
316
|
+
as={Button}
|
|
317
|
+
type="button"
|
|
318
|
+
variant="ghost"
|
|
319
|
+
size="icon-sm"
|
|
320
|
+
aria-label="Message input options"
|
|
321
|
+
>
|
|
322
|
+
<MoreHorizontal />
|
|
323
|
+
</DropdownMenuTrigger>
|
|
324
|
+
<DropdownMenuContent
|
|
325
|
+
onCloseAutoFocus={(event) => {
|
|
326
|
+
if (focusTarget) event.preventDefault()
|
|
327
|
+
}}
|
|
328
|
+
>
|
|
329
|
+
{props.menuItems}
|
|
330
|
+
<Show when={props.collapse}>
|
|
331
|
+
<DropdownMenuItem onSelect={() => changeCollapsed(true)}>
|
|
332
|
+
<ChevronsDownUp />
|
|
333
|
+
<div class="flex min-w-0 flex-col">
|
|
334
|
+
<span>Collapse the message input</span>
|
|
335
|
+
<span class="text-muted-foreground text-xs">
|
|
336
|
+
Make room to read; keep your draft
|
|
337
|
+
</span>
|
|
338
|
+
</div>
|
|
339
|
+
</DropdownMenuItem>
|
|
340
|
+
</Show>
|
|
341
|
+
</DropdownMenuContent>
|
|
342
|
+
</DropdownMenu>
|
|
343
|
+
</Show>
|
|
344
|
+
{props.leadingActions}
|
|
345
|
+
</div>
|
|
346
|
+
<div class="flex shrink-0 items-center gap-1">
|
|
347
|
+
{props.trailingActions}
|
|
348
|
+
<Show
|
|
349
|
+
when={props.busy}
|
|
350
|
+
fallback={
|
|
351
|
+
<Button
|
|
352
|
+
type="submit"
|
|
353
|
+
size="icon-sm"
|
|
354
|
+
disabled={!canSend()}
|
|
355
|
+
aria-label={pending() ? 'Sending message' : 'Send message'}
|
|
356
|
+
>
|
|
357
|
+
<Show when={pending()} fallback={<ArrowUp />}>
|
|
358
|
+
<Spinner size="sm" label={false} />
|
|
359
|
+
</Show>
|
|
360
|
+
</Button>
|
|
361
|
+
}
|
|
362
|
+
>
|
|
363
|
+
{(busy) => (
|
|
364
|
+
<BusySendButton
|
|
365
|
+
{...busy()}
|
|
366
|
+
alternateActionHint={
|
|
367
|
+
canSend(busy().mode === 'steer' ? 'queue' : 'steer')
|
|
368
|
+
? 'Ctrl/Cmd+Enter uses the other action'
|
|
369
|
+
: undefined
|
|
370
|
+
}
|
|
371
|
+
disabled={!canSend()}
|
|
372
|
+
onFire={() => {
|
|
373
|
+
void submit()
|
|
374
|
+
}}
|
|
375
|
+
/>
|
|
376
|
+
)}
|
|
377
|
+
</Show>
|
|
378
|
+
</div>
|
|
379
|
+
</div>
|
|
380
|
+
</form>
|
|
381
|
+
<Show when={props.context}>
|
|
382
|
+
<div data-slot="composer-context" class="flex min-w-0 items-center gap-2 pt-1">
|
|
383
|
+
{props.context}
|
|
384
|
+
</div>
|
|
385
|
+
</Show>
|
|
386
|
+
</Show>
|
|
387
|
+
</Show>
|
|
388
|
+
</div>
|
|
389
|
+
)
|
|
390
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export const PASTE_PREVIEW_MAX_LINES = 12
|
|
2
|
+
export const PASTE_PREVIEW_MAX_CHARACTERS = 1200
|
|
3
|
+
|
|
4
|
+
export interface PastePreviewSummary {
|
|
5
|
+
text: string
|
|
6
|
+
hiddenLines: number
|
|
7
|
+
hiddenCharacters: number
|
|
8
|
+
scannedCharacters: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Build a bounded preview without splitting or joining the full paste content. */
|
|
12
|
+
export function summarizePastePreview(content: string, totalLines: number): PastePreviewSummary {
|
|
13
|
+
const scanLimit = Math.min(content.length, PASTE_PREVIEW_MAX_CHARACTERS)
|
|
14
|
+
let end = 0
|
|
15
|
+
let visibleLines = 1
|
|
16
|
+
let scannedCharacters = 0
|
|
17
|
+
|
|
18
|
+
while (end < scanLimit) {
|
|
19
|
+
const character = content.charCodeAt(end)
|
|
20
|
+
scannedCharacters += 1
|
|
21
|
+
if (character === 10) {
|
|
22
|
+
if (visibleLines === PASTE_PREVIEW_MAX_LINES) break
|
|
23
|
+
visibleLines += 1
|
|
24
|
+
}
|
|
25
|
+
end += 1
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const text = content.slice(0, end)
|
|
29
|
+
return {
|
|
30
|
+
text,
|
|
31
|
+
hiddenLines: Math.max(0, totalLines - visibleLines),
|
|
32
|
+
hiddenCharacters: Math.max(0, content.length - text.length),
|
|
33
|
+
scannedCharacters,
|
|
34
|
+
}
|
|
35
|
+
}
|