@juspay/svelte-ui-components 4.6.0 → 4.8.0
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/HITL/HITL.svelte +50 -2
- package/dist/HITL/properties.d.ts +28 -0
- package/dist/Snippet/Snippet.svelte +16 -3
- package/dist/Snippet/copyResetTimer.d.ts +21 -0
- package/dist/Snippet/copyResetTimer.js +20 -0
- package/dist/Snippet/properties.d.ts +4 -0
- package/dist-wc/index.js +221 -126
- package/package.json +1 -1
package/dist/HITL/HITL.svelte
CHANGED
|
@@ -5,7 +5,13 @@
|
|
|
5
5
|
import Card from '../Card/Card.svelte';
|
|
6
6
|
import Progress from '../Progress/Progress.svelte';
|
|
7
7
|
import { pauseAllConfirmationTimers } from './timers';
|
|
8
|
-
import type {
|
|
8
|
+
import type {
|
|
9
|
+
HITLAction,
|
|
10
|
+
HITLExtraAction,
|
|
11
|
+
HITLProperties,
|
|
12
|
+
HITLResponse,
|
|
13
|
+
HITLSection
|
|
14
|
+
} from './properties';
|
|
9
15
|
|
|
10
16
|
/**
|
|
11
17
|
* Human-in-the-loop approval card: the assistant wants to run an action and the
|
|
@@ -43,6 +49,8 @@
|
|
|
43
49
|
cancelTestId,
|
|
44
50
|
completionTestId,
|
|
45
51
|
completionTextTestId,
|
|
52
|
+
actions,
|
|
53
|
+
children,
|
|
46
54
|
classes
|
|
47
55
|
}: HITLProperties = $props();
|
|
48
56
|
|
|
@@ -180,6 +188,20 @@
|
|
|
180
188
|
await decide(action);
|
|
181
189
|
};
|
|
182
190
|
|
|
191
|
+
// An extra action (e.g. "Deny with instructions") is a side path, not a
|
|
192
|
+
// decision — it never settles the card, so `onconfirm` never fires for it.
|
|
193
|
+
// It still counts as interacting with the card: pause this card's own
|
|
194
|
+
// countdown and every sibling's, exactly like clicking confirm or cancel.
|
|
195
|
+
const selectExtraAction = (action: HITLExtraAction): void => {
|
|
196
|
+
if (decisionPending || isProcessing || isCompleted) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
pauseAllConfirmationTimers.set(true);
|
|
200
|
+
stopCountdown();
|
|
201
|
+
clearAutoCancel();
|
|
202
|
+
action.onSelect();
|
|
203
|
+
};
|
|
204
|
+
|
|
183
205
|
onMount(() => {
|
|
184
206
|
if (isHistoryMode) {
|
|
185
207
|
return;
|
|
@@ -375,6 +397,11 @@
|
|
|
375
397
|
</span>
|
|
376
398
|
</div>
|
|
377
399
|
{:else}
|
|
400
|
+
{#if children}
|
|
401
|
+
<div class="extra-content">
|
|
402
|
+
{@render children()}
|
|
403
|
+
</div>
|
|
404
|
+
{/if}
|
|
378
405
|
<div class="action-buttons">
|
|
379
406
|
<div class="cancel-button">
|
|
380
407
|
<Button
|
|
@@ -385,6 +412,19 @@
|
|
|
385
412
|
onclick={() => interact('rejected')}
|
|
386
413
|
/>
|
|
387
414
|
</div>
|
|
415
|
+
{#each actions ?? [] as action, index (action.testId ?? `${index}-${action.label}`)}
|
|
416
|
+
<div class="extra-action-button">
|
|
417
|
+
<Button
|
|
418
|
+
variant="secondary"
|
|
419
|
+
text={action.label}
|
|
420
|
+
enable={!isProcessing}
|
|
421
|
+
testId={action.testId ?? (testId && `${testId}-action-${index}`)}
|
|
422
|
+
classes={action.classes}
|
|
423
|
+
ariaLabel={action.ariaLabel}
|
|
424
|
+
onclick={() => selectExtraAction(action)}
|
|
425
|
+
/>
|
|
426
|
+
</div>
|
|
427
|
+
{/each}
|
|
388
428
|
<div class="confirm-button">
|
|
389
429
|
{#if countdownActive}
|
|
390
430
|
<div class="progress-anchor">
|
|
@@ -480,6 +520,13 @@
|
|
|
480
520
|
text-transform: capitalize;
|
|
481
521
|
}
|
|
482
522
|
|
|
523
|
+
/* Rendered only when `children` is supplied; the flex gap on
|
|
524
|
+
.confirmation-body already spaces it from the params above and the
|
|
525
|
+
action-buttons row below, so no default-behavior change when it's absent. */
|
|
526
|
+
.extra-content {
|
|
527
|
+
width: 100%;
|
|
528
|
+
}
|
|
529
|
+
|
|
483
530
|
.action-buttons {
|
|
484
531
|
display: flex;
|
|
485
532
|
gap: var(--hitl-buttons-gap, 0.75rem);
|
|
@@ -487,7 +534,8 @@
|
|
|
487
534
|
}
|
|
488
535
|
|
|
489
536
|
.cancel-button,
|
|
490
|
-
.confirm-button
|
|
537
|
+
.confirm-button,
|
|
538
|
+
.extra-action-button {
|
|
491
539
|
flex: 1;
|
|
492
540
|
position: relative;
|
|
493
541
|
--button-width: 100%;
|
|
@@ -15,6 +15,23 @@ export type HITLInitialState = {
|
|
|
15
15
|
/** `'EXPIRED'` renders the timed-out completion state. */
|
|
16
16
|
status?: string;
|
|
17
17
|
};
|
|
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 = {
|
|
26
|
+
label: string;
|
|
27
|
+
onSelect: () => void;
|
|
28
|
+
/** `data-pw` for this action's button (default: `<testId>-action-<index>`). */
|
|
29
|
+
testId?: string;
|
|
30
|
+
/** Class string on this action's Button, e.g. to pick a variant. */
|
|
31
|
+
classes?: string;
|
|
32
|
+
/** Accessible name for this action's Button, when `label` alone isn't descriptive enough. */
|
|
33
|
+
ariaLabel?: string;
|
|
34
|
+
};
|
|
18
35
|
export type HITLProperties = OptionalHITLProperties & MandatoryHITLProperties;
|
|
19
36
|
export type MandatoryHITLProperties = {
|
|
20
37
|
confirmationId: string;
|
|
@@ -67,5 +84,16 @@ export type OptionalHITLProperties = {
|
|
|
67
84
|
completionTestId?: string;
|
|
68
85
|
/** Override the completion text's test id (default: `<testId>-completion-text`). */
|
|
69
86
|
completionTextTestId?: string;
|
|
87
|
+
/**
|
|
88
|
+
* Extra side actions rendered between cancel and confirm — e.g. "Deny with
|
|
89
|
+
* instructions". None of these settle the card; see `HITLExtraAction`.
|
|
90
|
+
*/
|
|
91
|
+
actions?: HITLExtraAction[];
|
|
92
|
+
/**
|
|
93
|
+
* Arbitrary extra controls — free-text input, multi-select chips — rendered
|
|
94
|
+
* in the body above the action row while the card is pending. Never shown
|
|
95
|
+
* once the card is completed or in history mode.
|
|
96
|
+
*/
|
|
97
|
+
children?: Snippet;
|
|
70
98
|
classes?: string;
|
|
71
99
|
};
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { onDestroy } from 'svelte';
|
|
2
3
|
import type { SnippetProperties } from './properties';
|
|
3
4
|
import Button from '../Button/Button.svelte';
|
|
4
5
|
import copySvg from '../assets/copy.svg?raw';
|
|
6
|
+
import { createCopyResetTimer } from './copyResetTimer';
|
|
5
7
|
|
|
6
8
|
let {
|
|
7
9
|
text,
|
|
@@ -9,20 +11,31 @@
|
|
|
9
11
|
showCopyButton = true,
|
|
10
12
|
testId,
|
|
11
13
|
copyIcon,
|
|
14
|
+
copiedLabel = 'Copied!',
|
|
15
|
+
copyResetMs = 2000,
|
|
12
16
|
oncopy,
|
|
13
17
|
classes
|
|
14
18
|
}: SnippetProperties = $props();
|
|
15
19
|
|
|
16
20
|
let copied = $state(false);
|
|
17
21
|
|
|
22
|
+
// The reset timer used to be a bare setTimeout with nothing clearing it: a
|
|
23
|
+
// second copy before the first one's timer fired left two timers racing to
|
|
24
|
+
// flip `copied`, and navigating away mid-flash left the timer armed against
|
|
25
|
+
// a destroyed component. `createCopyResetTimer` (unit-tested on its own in
|
|
26
|
+
// copyResetTimer.test.ts) guarantees only one timer is ever pending, and
|
|
27
|
+
// onDestroy guarantees it never outlives the component it targets.
|
|
28
|
+
const resetTimer = createCopyResetTimer();
|
|
29
|
+
onDestroy(resetTimer.cancel);
|
|
30
|
+
|
|
18
31
|
async function handleCopy() {
|
|
19
32
|
try {
|
|
20
33
|
await navigator.clipboard.writeText(text);
|
|
21
34
|
copied = true;
|
|
22
35
|
oncopy?.();
|
|
23
|
-
|
|
36
|
+
resetTimer.arm(() => {
|
|
24
37
|
copied = false;
|
|
25
|
-
},
|
|
38
|
+
}, copyResetMs);
|
|
26
39
|
} catch {
|
|
27
40
|
// Clipboard API unavailable (non-secure context, iframe restrictions)
|
|
28
41
|
}
|
|
@@ -42,7 +55,7 @@
|
|
|
42
55
|
<div class="snippet-copy">
|
|
43
56
|
<Button onclick={handleCopy} ariaLabel="Copy to clipboard">
|
|
44
57
|
{#if copied}
|
|
45
|
-
<span class="snippet-copied">
|
|
58
|
+
<span class="snippet-copied" aria-live="polite">{copiedLabel}</span>
|
|
46
59
|
{:else if typeof copyIcon === 'function'}
|
|
47
60
|
{@render copyIcon()}
|
|
48
61
|
{:else}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manages the single "revert the copied-feedback flag" timer behind Snippet's
|
|
3
|
+
* copy button (#530). Extracted as a plain, Svelte-free module so the timer
|
|
4
|
+
* safety this fixes -- never leaving two timers racing after a fast double
|
|
5
|
+
* copy, never firing against a component that has already unmounted -- is
|
|
6
|
+
* unit-testable on its own, the same way `Tooltip/tooltip-action.ts` keeps
|
|
7
|
+
* its DOM-timing logic outside the component.
|
|
8
|
+
*/
|
|
9
|
+
export type CopyResetTimer = {
|
|
10
|
+
/**
|
|
11
|
+
* (Re)arms the timer: clears any timer already pending, then schedules
|
|
12
|
+
* `onReset` to run after `delayMs`.
|
|
13
|
+
*/
|
|
14
|
+
arm: (onReset: () => void, delayMs: number) => void;
|
|
15
|
+
/**
|
|
16
|
+
* Cancels a pending timer, if any. Safe to call when nothing is pending --
|
|
17
|
+
* this is what Snippet calls from `onDestroy`.
|
|
18
|
+
*/
|
|
19
|
+
cancel: () => void;
|
|
20
|
+
};
|
|
21
|
+
export declare function createCopyResetTimer(): CopyResetTimer;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function createCopyResetTimer() {
|
|
2
|
+
let handle = null;
|
|
3
|
+
return {
|
|
4
|
+
arm(onReset, delayMs) {
|
|
5
|
+
if (handle !== null) {
|
|
6
|
+
clearTimeout(handle);
|
|
7
|
+
}
|
|
8
|
+
handle = setTimeout(() => {
|
|
9
|
+
handle = null;
|
|
10
|
+
onReset();
|
|
11
|
+
}, delayMs);
|
|
12
|
+
},
|
|
13
|
+
cancel() {
|
|
14
|
+
if (handle !== null) {
|
|
15
|
+
clearTimeout(handle);
|
|
16
|
+
handle = null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -8,6 +8,10 @@ export type OptionalSnippetProperties = {
|
|
|
8
8
|
showCopyButton?: boolean;
|
|
9
9
|
testId?: string;
|
|
10
10
|
copyIcon?: Snippet;
|
|
11
|
+
/** Text shown in place of the copy icon after a successful copy. Defaults to `'Copied!'`. */
|
|
12
|
+
copiedLabel?: string;
|
|
13
|
+
/** Milliseconds before the copied feedback reverts to the copy icon. Defaults to `2000`. */
|
|
14
|
+
copyResetMs?: number;
|
|
11
15
|
classes?: string;
|
|
12
16
|
};
|
|
13
17
|
export type SnippetEventProperties = {
|