@juspay/svelte-ui-components 4.33.3 → 4.34.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/Accordion/Accordion.svelte +26 -2
- package/dist/Accordion/properties.d.ts +7 -0
- package/dist/ArmedButton/ArmedButton.svelte +147 -0
- package/dist/ArmedButton/ArmedButton.svelte.d.ts +4 -0
- package/dist/ArmedButton/properties.d.ts +24 -0
- package/dist/ArmedButton/properties.js +1 -0
- package/dist/Banner/Banner.svelte +13 -8
- package/dist/Banner/properties.d.ts +8 -1
- package/dist/Button/Button.svelte +26 -0
- package/dist/Button/properties.d.ts +17 -0
- package/dist/ChatComposer/ChatComposer.svelte +314 -1
- package/dist/ChatComposer/properties.d.ts +42 -0
- package/dist/ChatMessageList/ChatMessageList.svelte +22 -2
- package/dist/ChatMessageList/ChatMessageList.svelte.d.ts +1 -1
- package/dist/CommandMenu/CommandMenu.svelte +53 -20
- package/dist/CommandMenu/properties.d.ts +24 -0
- package/dist/DiffViewer/DiffViewer.svelte +210 -0
- package/dist/DiffViewer/DiffViewer.svelte.d.ts +4 -0
- package/dist/DiffViewer/properties.d.ts +25 -0
- package/dist/DiffViewer/properties.js +1 -0
- package/dist/EmptyState/EmptyState.svelte +22 -3
- package/dist/EmptyState/properties.d.ts +9 -0
- package/dist/HITL/HITL.svelte +320 -51
- package/dist/HITL/properties.d.ts +113 -11
- package/dist/Input/Input.svelte +45 -1
- package/dist/Input/properties.d.ts +15 -0
- package/dist/KeyValue/KeyValue.svelte +16 -3
- package/dist/KeyValue/properties.d.ts +2 -0
- package/dist/Progress/Progress.svelte +96 -41
- package/dist/Progress/properties.d.ts +19 -0
- package/dist/Snippet/Snippet.svelte +45 -1
- package/dist/Snippet/properties.d.ts +8 -0
- package/dist/Tabs/Tabs.svelte +75 -16
- package/dist/Tabs/properties.d.ts +21 -0
- package/dist/ThinkingIndicator/ThinkingIndicator.svelte.d.ts +1 -1
- package/dist/Toolbar/Toolbar.svelte +81 -3
- package/dist/Toolbar/properties.d.ts +37 -0
- package/dist/WindowedList/WindowedList.svelte +129 -0
- package/dist/WindowedList/WindowedList.svelte.d.ts +25 -0
- package/dist/WindowedList/properties.d.ts +38 -0
- package/dist/WindowedList/properties.js +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +3 -0
- package/dist/styles/theme-dark.css +42 -0
- package/dist-wc/index.js +2637 -1642
- package/package.json +2 -1
package/dist/HITL/HITL.svelte
CHANGED
|
@@ -3,16 +3,24 @@
|
|
|
3
3
|
import { get } from 'svelte/store';
|
|
4
4
|
import Button from '../Button/Button.svelte';
|
|
5
5
|
import Card from '../Card/Card.svelte';
|
|
6
|
+
import Input from '../Input/Input.svelte';
|
|
6
7
|
import Progress from '../Progress/Progress.svelte';
|
|
7
8
|
import { pauseAllConfirmationTimers } from './timers';
|
|
8
9
|
import type {
|
|
9
10
|
HITLAction,
|
|
11
|
+
HITLAskForTextExtraAction,
|
|
10
12
|
HITLExtraAction,
|
|
11
13
|
HITLProperties,
|
|
14
|
+
HITLQuestion,
|
|
15
|
+
HITLQuestionAnswer,
|
|
12
16
|
HITLResponse,
|
|
13
17
|
HITLSection
|
|
14
18
|
} from './properties';
|
|
15
19
|
|
|
20
|
+
// What an extra action or an answer adds to the settled event;
|
|
21
|
+
// confirm/cancel add nothing.
|
|
22
|
+
type HITLSettleDetail = { value?: string; message?: string; answers?: HITLQuestionAnswer[] };
|
|
23
|
+
|
|
16
24
|
/**
|
|
17
25
|
* Human-in-the-loop approval card: the assistant wants to run an action and the
|
|
18
26
|
* person approves or cancels it. Unless disabled, a countdown sweeps across the
|
|
@@ -29,6 +37,9 @@
|
|
|
29
37
|
onconfirm,
|
|
30
38
|
confirmLabel = 'Confirm',
|
|
31
39
|
cancelLabel = 'Cancel',
|
|
40
|
+
showCancel = true,
|
|
41
|
+
showConfirm = true,
|
|
42
|
+
showEmptyParameters = true,
|
|
32
43
|
countdownSeconds = 10,
|
|
33
44
|
autoCancelSeconds = 0,
|
|
34
45
|
isMicMuted = false,
|
|
@@ -50,6 +61,12 @@
|
|
|
50
61
|
completionTestId,
|
|
51
62
|
completionTextTestId,
|
|
52
63
|
actions,
|
|
64
|
+
questions,
|
|
65
|
+
answerOnSelect = false,
|
|
66
|
+
questionsSettleAs = 'approved',
|
|
67
|
+
sendAnswersLabel = 'Send answers',
|
|
68
|
+
optionClasses,
|
|
69
|
+
selectedOptionClasses,
|
|
53
70
|
children,
|
|
54
71
|
classes
|
|
55
72
|
}: HITLProperties = $props();
|
|
@@ -134,7 +151,7 @@
|
|
|
134
151
|
// updated in between.
|
|
135
152
|
let decisionPending = false;
|
|
136
153
|
|
|
137
|
-
const settle = (action: HITLAction): void => {
|
|
154
|
+
const settle = (action: HITLAction, detail: HITLSettleDetail = {}): void => {
|
|
138
155
|
if (isProcessing || isCompleted) {
|
|
139
156
|
return;
|
|
140
157
|
}
|
|
@@ -144,7 +161,12 @@
|
|
|
144
161
|
onconfirm?.({
|
|
145
162
|
confirmationId,
|
|
146
163
|
action,
|
|
147
|
-
approved: action !== 'rejected'
|
|
164
|
+
approved: action !== 'rejected',
|
|
165
|
+
// Only the keys that were set, so a plain confirm/cancel event keeps
|
|
166
|
+
// exactly its old three-key shape.
|
|
167
|
+
...(typeof detail.value === 'string' ? { value: detail.value } : {}),
|
|
168
|
+
...(typeof detail.message === 'string' ? { message: detail.message } : {}),
|
|
169
|
+
...(Array.isArray(detail.answers) ? { answers: detail.answers } : {})
|
|
148
170
|
});
|
|
149
171
|
} catch {
|
|
150
172
|
// The decision failed to hand off; release the card for another attempt.
|
|
@@ -155,13 +177,13 @@
|
|
|
155
177
|
}
|
|
156
178
|
};
|
|
157
179
|
|
|
158
|
-
const decide = async (action: HITLAction): Promise<void> => {
|
|
180
|
+
const decide = async (action: HITLAction, detail: HITLSettleDetail = {}): Promise<void> => {
|
|
159
181
|
if (decisionPending || isCompleted) {
|
|
160
182
|
return;
|
|
161
183
|
}
|
|
162
184
|
decisionPending = true;
|
|
163
185
|
await restoreMicState();
|
|
164
|
-
settle(action);
|
|
186
|
+
settle(action, detail);
|
|
165
187
|
};
|
|
166
188
|
|
|
167
189
|
const startCountdown = (): void => {
|
|
@@ -181,27 +203,112 @@
|
|
|
181
203
|
}, 100);
|
|
182
204
|
};
|
|
183
205
|
|
|
184
|
-
|
|
206
|
+
// Any interaction pauses this card's own countdown and every sibling's.
|
|
207
|
+
const pauseTimers = (): void => {
|
|
185
208
|
pauseAllConfirmationTimers.set(true);
|
|
186
209
|
stopCountdown();
|
|
187
210
|
clearAutoCancel();
|
|
188
|
-
await decide(action);
|
|
189
211
|
};
|
|
190
212
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
213
|
+
const interact = async (action: HITLAction, detail: HITLSettleDetail = {}): Promise<void> => {
|
|
214
|
+
pauseTimers();
|
|
215
|
+
await decide(action, detail);
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
// The 'ask-for-text' action whose reply box is open, if any. While set, the
|
|
219
|
+
// box replaces the action row.
|
|
220
|
+
let replyAction = $state<HITLAskForTextExtraAction | null>(null);
|
|
221
|
+
let replyText = $state('');
|
|
222
|
+
const canSendReply = $derived(replyText.trim().length > 0 && !isProcessing);
|
|
223
|
+
|
|
224
|
+
// A side action never settles the card, so `onconfirm` never fires for it.
|
|
225
|
+
// 'decision' settles immediately; 'ask-for-text' settles once text is sent.
|
|
226
|
+
// All three count as interacting with the card.
|
|
195
227
|
const selectExtraAction = (action: HITLExtraAction): void => {
|
|
196
228
|
if (decisionPending || isProcessing || isCompleted) {
|
|
197
229
|
return;
|
|
198
230
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
231
|
+
if (action.kind === 'decision') {
|
|
232
|
+
void interact(action.settleAs, { value: action.value });
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
pauseTimers();
|
|
236
|
+
if (action.kind === 'ask-for-text') {
|
|
237
|
+
replyText = '';
|
|
238
|
+
replyAction = action;
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
202
241
|
action.onSelect();
|
|
203
242
|
};
|
|
204
243
|
|
|
244
|
+
const sendReply = (): void => {
|
|
245
|
+
const text = replyText.trim();
|
|
246
|
+
if (replyAction === null || text.length === 0) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
void decide(replyAction.settleAs, { value: replyAction.value, message: text });
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const closeReply = (): void => {
|
|
253
|
+
replyAction = null;
|
|
254
|
+
replyText = '';
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
// Picks per question, by index.
|
|
258
|
+
let picks = $state<string[][]>([]);
|
|
259
|
+
const questionList = $derived(questions ?? []);
|
|
260
|
+
// A single-select question needs no Send when answerOnSelect settles it on
|
|
261
|
+
// click, so Send exists only if something is left to collect.
|
|
262
|
+
const settlesOnClick = (question: HITLQuestion): boolean =>
|
|
263
|
+
answerOnSelect && question.multiSelect !== true;
|
|
264
|
+
const showSendAnswers = $derived(
|
|
265
|
+
questionList.length > 0 && questionList.some((question) => !settlesOnClick(question))
|
|
266
|
+
);
|
|
267
|
+
const canSendAnswers = $derived(
|
|
268
|
+
!isProcessing &&
|
|
269
|
+
questionList.every(
|
|
270
|
+
(question, index) => settlesOnClick(question) || (picks[index] ?? []).length > 0
|
|
271
|
+
)
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
const answerFor = (question: HITLQuestion, picked: string[]): HITLQuestionAnswer => ({
|
|
275
|
+
...(typeof question.header === 'string' ? { header: question.header } : {}),
|
|
276
|
+
question: question.question,
|
|
277
|
+
picks: picked
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
const pickOption = (questionIndex: number, label: string): void => {
|
|
281
|
+
const question = questionList.at(questionIndex);
|
|
282
|
+
if (decisionPending || isProcessing || isCompleted || typeof question !== 'object') {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (settlesOnClick(question)) {
|
|
286
|
+
void interact(questionsSettleAs, { answers: [answerFor(question, [label])] });
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
pauseTimers();
|
|
290
|
+
const current = picks[questionIndex] ?? [];
|
|
291
|
+
const has = current.includes(label);
|
|
292
|
+
const next =
|
|
293
|
+
question.multiSelect === true
|
|
294
|
+
? has
|
|
295
|
+
? current.filter((picked) => picked !== label)
|
|
296
|
+
: [...current, label]
|
|
297
|
+
: has
|
|
298
|
+
? []
|
|
299
|
+
: [label];
|
|
300
|
+
picks = questionList.map((_, index) => (index === questionIndex ? next : (picks[index] ?? [])));
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const sendAnswers = (): void => {
|
|
304
|
+
if (!canSendAnswers) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
void interact(questionsSettleAs, {
|
|
308
|
+
answers: questionList.map((question, index) => answerFor(question, picks[index] ?? []))
|
|
309
|
+
});
|
|
310
|
+
};
|
|
311
|
+
|
|
205
312
|
onMount(() => {
|
|
206
313
|
if (isHistoryMode) {
|
|
207
314
|
return;
|
|
@@ -309,17 +416,31 @@
|
|
|
309
416
|
built.push({ label: key.replace(/([A-Z])/g, ' $1').trim(), value: formatted });
|
|
310
417
|
}
|
|
311
418
|
}
|
|
312
|
-
return built
|
|
419
|
+
return built;
|
|
313
420
|
};
|
|
314
421
|
|
|
422
|
+
// What a card with nothing to list shows. A card that asks questions already
|
|
423
|
+
// has content of its own, and showEmptyParameters={false} opts a card out
|
|
424
|
+
// explicitly. `children` is deliberately NOT a reason to hide it: it is a
|
|
425
|
+
// pre-existing prop, so treating it as one would silently change the output
|
|
426
|
+
// of every existing children-only consumer -- including one whose children
|
|
427
|
+
// snippet renders nothing yet (e.g. gated on its own internal state), which
|
|
428
|
+
// is exactly the "No parameters" case this placeholder exists for.
|
|
429
|
+
const emptyParameters = $derived<HITLSection[]>(
|
|
430
|
+
questionList.length > 0 || !showEmptyParameters
|
|
431
|
+
? []
|
|
432
|
+
: [{ label: 'PARAMETERS', value: 'No parameters' }]
|
|
433
|
+
);
|
|
434
|
+
|
|
315
435
|
const parameterSections = $derived.by((): HITLSection[] => {
|
|
316
436
|
if (sections && sections.length > 0) {
|
|
317
437
|
return sections;
|
|
318
438
|
}
|
|
319
439
|
if (functionArguments && Object.keys(functionArguments).length > 0) {
|
|
320
|
-
|
|
440
|
+
const formatted = formatArguments(functionArguments);
|
|
441
|
+
return formatted.length > 0 ? formatted : emptyParameters;
|
|
321
442
|
}
|
|
322
|
-
return
|
|
443
|
+
return emptyParameters;
|
|
323
444
|
});
|
|
324
445
|
</script>
|
|
325
446
|
|
|
@@ -402,43 +523,138 @@
|
|
|
402
523
|
{@render children()}
|
|
403
524
|
</div>
|
|
404
525
|
{/if}
|
|
405
|
-
|
|
406
|
-
<div class="
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
526
|
+
{#if questionList.length > 0}
|
|
527
|
+
<div class="questions">
|
|
528
|
+
{#each questionList as question, questionIndex (questionIndex)}
|
|
529
|
+
<div
|
|
530
|
+
class="question"
|
|
531
|
+
role="group"
|
|
532
|
+
aria-label={question.header ?? question.question}
|
|
533
|
+
data-pw={testId && `${testId}-question-${questionIndex}`}
|
|
534
|
+
>
|
|
535
|
+
{#if typeof question.header === 'string'}
|
|
536
|
+
<span class="question-header">{question.header}</span>
|
|
537
|
+
{/if}
|
|
538
|
+
<span class="question-text">{question.question}</span>
|
|
539
|
+
<div class="question-options">
|
|
540
|
+
{#each question.options as option, optionIndex (option.label)}
|
|
541
|
+
{@const picked = (picks[questionIndex] ?? []).includes(option.label)}
|
|
542
|
+
<Button
|
|
543
|
+
variant="secondary"
|
|
544
|
+
text={option.label}
|
|
545
|
+
title={option.description}
|
|
546
|
+
{...settlesOnClick(question) ? {} : { ariaPressed: picked }}
|
|
547
|
+
enable={!isProcessing}
|
|
548
|
+
classes={[optionClasses, picked ? selectedOptionClasses : '']
|
|
549
|
+
.filter((value) => typeof value === 'string' && value.length > 0)
|
|
550
|
+
.join(' ')}
|
|
551
|
+
testId={testId && `${testId}-question-${questionIndex}-option-${optionIndex}`}
|
|
552
|
+
onclick={() => pickOption(questionIndex, option.label)}
|
|
553
|
+
/>
|
|
554
|
+
{/each}
|
|
555
|
+
</div>
|
|
556
|
+
</div>
|
|
557
|
+
{/each}
|
|
414
558
|
</div>
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
559
|
+
{/if}
|
|
560
|
+
{#if replyAction !== null}
|
|
561
|
+
<div class="reply-box" data-pw={testId && `${testId}-reply`}>
|
|
562
|
+
<Input
|
|
563
|
+
bind:value={replyText}
|
|
564
|
+
placeholder={replyAction.placeholder ?? ''}
|
|
565
|
+
ariaLabel={replyAction.ariaLabel ?? replyAction.label}
|
|
566
|
+
autofocus
|
|
567
|
+
useTextArea={replyAction.multiline === true}
|
|
568
|
+
autoResize={replyAction.multiline === true}
|
|
569
|
+
rows={3}
|
|
570
|
+
minRows={3}
|
|
571
|
+
maxRows={10}
|
|
572
|
+
testId={testId && `${testId}-reply-input`}
|
|
573
|
+
onkeydown={(event) => {
|
|
574
|
+
// One line: Enter sends. Multiline: Enter is a newline and
|
|
575
|
+
// Ctrl/Cmd+Enter sends.
|
|
576
|
+
const sends =
|
|
577
|
+
replyAction?.multiline === true ? event.metaKey || event.ctrlKey : true;
|
|
578
|
+
if (event.key === 'Enter' && sends) {
|
|
579
|
+
event.preventDefault();
|
|
580
|
+
sendReply();
|
|
581
|
+
}
|
|
582
|
+
}}
|
|
583
|
+
/>
|
|
584
|
+
<div class="action-buttons">
|
|
585
|
+
<div class="cancel-button">
|
|
586
|
+
<Button
|
|
587
|
+
variant="secondary"
|
|
588
|
+
text={replyAction.backLabel ?? 'Back'}
|
|
589
|
+
enable={!isProcessing}
|
|
590
|
+
testId={testId && `${testId}-reply-back`}
|
|
591
|
+
onclick={closeReply}
|
|
592
|
+
/>
|
|
593
|
+
</div>
|
|
594
|
+
<div class="confirm-button">
|
|
595
|
+
<Button
|
|
596
|
+
text={replyAction.submitLabel ?? 'Send'}
|
|
597
|
+
classes={replyAction.submitClasses}
|
|
598
|
+
enable={canSendReply}
|
|
599
|
+
testId={testId && `${testId}-reply-send`}
|
|
600
|
+
onclick={sendReply}
|
|
601
|
+
/>
|
|
602
|
+
</div>
|
|
426
603
|
</div>
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
604
|
+
</div>
|
|
605
|
+
{:else}
|
|
606
|
+
<div class="action-buttons">
|
|
607
|
+
{#if showCancel}
|
|
608
|
+
<div class="cancel-button">
|
|
609
|
+
<Button
|
|
610
|
+
variant="secondary"
|
|
611
|
+
text={cancelLabel}
|
|
612
|
+
enable={!isProcessing}
|
|
613
|
+
testId={cancelTestId ?? (testId && `${testId}-cancel`)}
|
|
614
|
+
onclick={() => interact('rejected')}
|
|
615
|
+
/>
|
|
616
|
+
</div>
|
|
617
|
+
{/if}
|
|
618
|
+
{#each actions ?? [] as action, index (action.testId ?? `${index}-${action.label}`)}
|
|
619
|
+
<div class="extra-action-button">
|
|
620
|
+
<Button
|
|
621
|
+
variant="secondary"
|
|
622
|
+
text={action.label}
|
|
623
|
+
enable={!isProcessing}
|
|
624
|
+
testId={action.testId ?? (testId && `${testId}-action-${index}`)}
|
|
625
|
+
classes={action.classes}
|
|
626
|
+
ariaLabel={action.ariaLabel}
|
|
627
|
+
onclick={() => selectExtraAction(action)}
|
|
628
|
+
/>
|
|
629
|
+
</div>
|
|
630
|
+
{/each}
|
|
631
|
+
{#if showSendAnswers}
|
|
632
|
+
<div class="confirm-button">
|
|
633
|
+
<Button
|
|
634
|
+
text={sendAnswersLabel}
|
|
635
|
+
enable={canSendAnswers}
|
|
636
|
+
testId={testId && `${testId}-send-answers`}
|
|
637
|
+
onclick={sendAnswers}
|
|
638
|
+
/>
|
|
639
|
+
</div>
|
|
640
|
+
{/if}
|
|
641
|
+
{#if showConfirm}
|
|
642
|
+
<div class="confirm-button">
|
|
643
|
+
{#if countdownActive}
|
|
644
|
+
<div class="progress-anchor">
|
|
645
|
+
<Progress value={elapsedTime} max={countdownSeconds} />
|
|
646
|
+
</div>
|
|
647
|
+
{/if}
|
|
648
|
+
<Button
|
|
649
|
+
text={confirmLabel}
|
|
650
|
+
enable={!isProcessing}
|
|
651
|
+
testId={confirmTestId ?? (testId && `${testId}-confirm`)}
|
|
652
|
+
onclick={() => interact('approved')}
|
|
653
|
+
/>
|
|
432
654
|
</div>
|
|
433
655
|
{/if}
|
|
434
|
-
<Button
|
|
435
|
-
text={confirmLabel}
|
|
436
|
-
enable={!isProcessing}
|
|
437
|
-
testId={confirmTestId ?? (testId && `${testId}-confirm`)}
|
|
438
|
-
onclick={() => interact('approved')}
|
|
439
|
-
/>
|
|
440
656
|
</div>
|
|
441
|
-
|
|
657
|
+
{/if}
|
|
442
658
|
{/if}
|
|
443
659
|
</div>
|
|
444
660
|
</Card>
|
|
@@ -518,7 +734,9 @@
|
|
|
518
734
|
line-height: var(--hitl-param-value-line-height, 1.4);
|
|
519
735
|
color: var(--hitl-param-value-color, #1f1f23);
|
|
520
736
|
word-spacing: var(--hitl-param-value-word-spacing, normal);
|
|
521
|
-
|
|
737
|
+
/* Capitalize suits prose values; a command or a path has to read verbatim,
|
|
738
|
+
so the consumer can turn it off. */
|
|
739
|
+
text-transform: var(--hitl-param-value-text-transform, capitalize);
|
|
522
740
|
}
|
|
523
741
|
|
|
524
742
|
/* Rendered only when `children` is supplied; the flex gap on
|
|
@@ -528,12 +746,63 @@
|
|
|
528
746
|
width: 100%;
|
|
529
747
|
}
|
|
530
748
|
|
|
749
|
+
/* The question and reply rules are written down the structure they live in
|
|
750
|
+
(.confirmation-body > ...), so a class of the same name that a consumer passes through
|
|
751
|
+
`classes`, which lands on the root, cannot match them. */
|
|
752
|
+
.confirmation-body > .questions {
|
|
753
|
+
display: flex;
|
|
754
|
+
flex-direction: column;
|
|
755
|
+
gap: var(--hitl-questions-gap, 0.75rem);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
.questions > .question {
|
|
759
|
+
display: flex;
|
|
760
|
+
flex-direction: column;
|
|
761
|
+
gap: var(--hitl-question-gap, 0.375rem);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/* Same voice as the parameter blocks above: the header reads like a
|
|
765
|
+
parameter label, the question like its value. */
|
|
766
|
+
.question > .question-header {
|
|
767
|
+
font-size: var(--hitl-param-label-font-size, 0.6875rem);
|
|
768
|
+
font-weight: var(--hitl-param-label-font-weight, 600);
|
|
769
|
+
letter-spacing: var(--hitl-param-label-letter-spacing, 0.04em);
|
|
770
|
+
color: var(--hitl-param-label-color, #595959);
|
|
771
|
+
text-transform: uppercase;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
.question > .question-text {
|
|
775
|
+
font-size: var(--hitl-param-value-font-size, 0.875rem);
|
|
776
|
+
line-height: var(--hitl-param-value-line-height, 1.4);
|
|
777
|
+
color: var(--hitl-param-value-color, #1f1f23);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
.question > .question-options {
|
|
781
|
+
display: flex;
|
|
782
|
+
flex-wrap: wrap;
|
|
783
|
+
gap: var(--hitl-question-options-gap, 0.5rem);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
.confirmation-body > .reply-box {
|
|
787
|
+
display: flex;
|
|
788
|
+
flex-direction: column;
|
|
789
|
+
gap: var(--hitl-reply-gap, 0.5rem);
|
|
790
|
+
width: 100%;
|
|
791
|
+
--input-container-width: 100%;
|
|
792
|
+
}
|
|
793
|
+
|
|
531
794
|
.action-buttons {
|
|
532
795
|
display: flex;
|
|
533
796
|
gap: var(--hitl-buttons-gap, 0.75rem);
|
|
534
797
|
width: 100%;
|
|
535
798
|
}
|
|
536
799
|
|
|
800
|
+
/* Zero specificity, so a consumer's own flex-wrap rule for the row keeps winning, as it
|
|
801
|
+
did before this hook existed. */
|
|
802
|
+
:where(.action-buttons) {
|
|
803
|
+
flex-wrap: var(--hitl-buttons-wrap, nowrap);
|
|
804
|
+
}
|
|
805
|
+
|
|
537
806
|
.cancel-button,
|
|
538
807
|
.confirm-button,
|
|
539
808
|
.extra-action-button {
|
|
@@ -644,8 +913,8 @@
|
|
|
644
913
|
loaders in Button and ListItem, where the base rule is the FINISHED state and
|
|
645
914
|
stopping the animation would claim completion that has not happened. */
|
|
646
915
|
@media (prefers-reduced-motion: reduce) {
|
|
647
|
-
.hitl
|
|
648
|
-
.
|
|
916
|
+
.hitl,
|
|
917
|
+
.completion {
|
|
649
918
|
animation: none;
|
|
650
919
|
}
|
|
651
920
|
}
|
|
@@ -9,22 +9,38 @@ export type HITLEvent = {
|
|
|
9
9
|
confirmationId: string;
|
|
10
10
|
action: HITLAction;
|
|
11
11
|
approved: boolean;
|
|
12
|
+
/** The `value` of the extra action that settled the card, when one did. */
|
|
13
|
+
value?: string;
|
|
14
|
+
/** The text submitted through an `'ask-for-text'` extra action. */
|
|
15
|
+
message?: string;
|
|
16
|
+
/** Set when the card was settled by answering `questions`. */
|
|
17
|
+
answers?: HITLQuestionAnswer[];
|
|
18
|
+
};
|
|
19
|
+
export type HITLQuestionOption = {
|
|
20
|
+
label: string;
|
|
21
|
+
/** Shown as the option's hover tooltip. */
|
|
22
|
+
description?: string;
|
|
23
|
+
};
|
|
24
|
+
export type HITLQuestion = {
|
|
25
|
+
/** Short name for the question, e.g. "Auth method". */
|
|
26
|
+
header?: string;
|
|
27
|
+
question: string;
|
|
28
|
+
options: HITLQuestionOption[];
|
|
29
|
+
/** Any number of options may be picked. Default `false`: one at most. */
|
|
30
|
+
multiSelect?: boolean;
|
|
31
|
+
};
|
|
32
|
+
export type HITLQuestionAnswer = {
|
|
33
|
+
header?: string;
|
|
34
|
+
question: string;
|
|
35
|
+
picks: string[];
|
|
12
36
|
};
|
|
13
37
|
export type HITLInitialState = {
|
|
14
38
|
approved?: boolean;
|
|
15
39
|
/** `'EXPIRED'` renders the timed-out completion state. */
|
|
16
40
|
status?: string;
|
|
17
41
|
};
|
|
18
|
-
|
|
19
|
-
* A side action rendered beside confirm/cancel — e.g. "Deny with instructions"
|
|
20
|
-
* opening a compose panel, or a third disposition a plain approve/reject can't
|
|
21
|
-
* express. Selecting one does NOT settle the card: no completion state, no
|
|
22
|
-
* `onconfirm`. It only pauses the countdown (own and siblings', like any other
|
|
23
|
-
* interaction) and calls `onSelect` — settling, if any, is the caller's job.
|
|
24
|
-
*/
|
|
25
|
-
export type HITLExtraAction = {
|
|
42
|
+
export type HITLExtraActionBase = {
|
|
26
43
|
label: string;
|
|
27
|
-
onSelect: () => void;
|
|
28
44
|
/** `data-pw` for this action's button (default: `<testId>-action-<index>`). */
|
|
29
45
|
testId?: string;
|
|
30
46
|
/** Class string on this action's Button, e.g. to pick a variant. */
|
|
@@ -32,6 +48,51 @@ export type HITLExtraAction = {
|
|
|
32
48
|
/** Accessible name for this action's Button, when `label` alone isn't descriptive enough. */
|
|
33
49
|
ariaLabel?: string;
|
|
34
50
|
};
|
|
51
|
+
/**
|
|
52
|
+
* The default kind: a side path beside confirm/cancel, e.g. opening the
|
|
53
|
+
* caller's own compose panel. Selecting one does NOT settle the card: no
|
|
54
|
+
* completion state, no `onconfirm`. It only pauses the countdown (own and
|
|
55
|
+
* siblings', like any other interaction) and calls `onSelect`. Settling, if
|
|
56
|
+
* any, is the caller's job.
|
|
57
|
+
*/
|
|
58
|
+
export type HITLSideExtraAction = HITLExtraActionBase & {
|
|
59
|
+
kind?: 'side';
|
|
60
|
+
onSelect: () => void;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* A decision with its own label: settles the card as `settleAs`, exactly like
|
|
64
|
+
* confirm/cancel, with `value` carried on the `onconfirm` event so the caller
|
|
65
|
+
* can tell several same-`settleAs` decisions apart (e.g. plan-approval modes).
|
|
66
|
+
*/
|
|
67
|
+
export type HITLDecisionExtraAction = HITLExtraActionBase & {
|
|
68
|
+
kind: 'decision';
|
|
69
|
+
settleAs: HITLAction;
|
|
70
|
+
value?: string;
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Asks for text before deciding: opens a reply box inside the card in place of
|
|
74
|
+
* the action row. Submitting settles as `settleAs` with the text on
|
|
75
|
+
* `HITLEvent.message`; the Back button closes the box without settling.
|
|
76
|
+
*/
|
|
77
|
+
export type HITLAskForTextExtraAction = HITLExtraActionBase & {
|
|
78
|
+
kind: 'ask-for-text';
|
|
79
|
+
settleAs: HITLAction;
|
|
80
|
+
value?: string;
|
|
81
|
+
placeholder?: string;
|
|
82
|
+
/** Default `'Send'`. */
|
|
83
|
+
submitLabel?: string;
|
|
84
|
+
/** Class string on the Send button, e.g. a danger variant for a deny. */
|
|
85
|
+
submitClasses?: string;
|
|
86
|
+
/** Default `'Back'`. */
|
|
87
|
+
backLabel?: string;
|
|
88
|
+
/**
|
|
89
|
+
* A growing textarea (3-10 rows) instead of a one-line input, for a reply
|
|
90
|
+
* that can run to paragraphs. Enter then adds a line; Send or
|
|
91
|
+
* Ctrl/Cmd+Enter sends. Default `false`.
|
|
92
|
+
*/
|
|
93
|
+
multiline?: boolean;
|
|
94
|
+
};
|
|
95
|
+
export type HITLExtraAction = HITLSideExtraAction | HITLDecisionExtraAction | HITLAskForTextExtraAction;
|
|
35
96
|
export type HITLProperties = OptionalHITLProperties & MandatoryHITLProperties;
|
|
36
97
|
export type MandatoryHITLProperties = {
|
|
37
98
|
confirmationId: string;
|
|
@@ -52,6 +113,24 @@ export type OptionalHITLProperties = {
|
|
|
52
113
|
onconfirm?: (event: HITLEvent) => void;
|
|
53
114
|
confirmLabel?: string;
|
|
54
115
|
cancelLabel?: string;
|
|
116
|
+
/** Hides the cancel button entirely — for a card whose only real dispositions come through `actions`. Default `true`. */
|
|
117
|
+
showCancel?: boolean;
|
|
118
|
+
/**
|
|
119
|
+
* Hides the confirm button — for a card whose only real dispositions come
|
|
120
|
+
* through `actions` (e.g. several labelled `'decision'` actions). The
|
|
121
|
+
* countdown sweep lives on that button, so pair it with `countdownSeconds={0}`.
|
|
122
|
+
* Default `true`.
|
|
123
|
+
*/
|
|
124
|
+
showConfirm?: boolean;
|
|
125
|
+
/**
|
|
126
|
+
* A card with nothing to list (no `sections`, and no `functionArguments`
|
|
127
|
+
* that survive `hiddenKeys`) shows a "PARAMETERS / No parameters"
|
|
128
|
+
* placeholder. `false` hides it -- for a card whose body is its own
|
|
129
|
+
* `children` (a plan, say) and genuinely has no parameters to show.
|
|
130
|
+
* Supplying `children` does not hide it by itself. A card that asks
|
|
131
|
+
* `questions` never shows it, either way. Default `true`.
|
|
132
|
+
*/
|
|
133
|
+
showEmptyParameters?: boolean;
|
|
55
134
|
/**
|
|
56
135
|
* Seconds until the card auto-approves, with a sweep across the confirm
|
|
57
136
|
* button. `0` disables auto-approval.
|
|
@@ -85,10 +164,33 @@ export type OptionalHITLProperties = {
|
|
|
85
164
|
/** Override the completion text's test id (default: `<testId>-completion-text`). */
|
|
86
165
|
completionTextTestId?: string;
|
|
87
166
|
/**
|
|
88
|
-
* Extra
|
|
89
|
-
*
|
|
167
|
+
* Extra actions rendered between cancel and confirm. By default each is a
|
|
168
|
+
* side path that does not settle the card; `kind: 'decision'` and
|
|
169
|
+
* `kind: 'ask-for-text'` settle it. See `HITLExtraAction`.
|
|
90
170
|
*/
|
|
91
171
|
actions?: HITLExtraAction[];
|
|
172
|
+
/**
|
|
173
|
+
* Questions answered by picking options inside the card. By default picks
|
|
174
|
+
* are collected (a single-select question toggles like a radio, a
|
|
175
|
+
* multi-select one like checkboxes) and the Send-answers button — enabled
|
|
176
|
+
* once every question has a pick — settles the card as `questionsSettleAs`
|
|
177
|
+
* with every question's picks on `HITLEvent.answers`.
|
|
178
|
+
*/
|
|
179
|
+
questions?: HITLQuestion[];
|
|
180
|
+
/**
|
|
181
|
+
* Settle as soon as a single-select option is clicked, with only that
|
|
182
|
+
* question's answer. Multi-select questions still collect picks, and Send
|
|
183
|
+
* then needs only those. Default `false`.
|
|
184
|
+
*/
|
|
185
|
+
answerOnSelect?: boolean;
|
|
186
|
+
/** What answering `questions` settles the card as. Default `'approved'`. */
|
|
187
|
+
questionsSettleAs?: HITLAction;
|
|
188
|
+
/** Default `'Send answers'`. */
|
|
189
|
+
sendAnswersLabel?: string;
|
|
190
|
+
/** Class string on every question option's Button, e.g. a chip recipe. */
|
|
191
|
+
optionClasses?: string;
|
|
192
|
+
/** Added to a picked option's Button, after `optionClasses`. */
|
|
193
|
+
selectedOptionClasses?: string;
|
|
92
194
|
/**
|
|
93
195
|
* Arbitrary extra controls — free-text input, multi-select chips — rendered
|
|
94
196
|
* in the body above the action row while the card is pending. Never shown
|