@juspay/svelte-ui-components 2.125.0 → 2.127.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.
@@ -0,0 +1,576 @@
1
+ <script lang="ts">
2
+ import { onMount, onDestroy } from 'svelte';
3
+ import { get } from 'svelte/store';
4
+ import Button from '../Button/Button.svelte';
5
+ import Card from '../Card/Card.svelte';
6
+ import Progress from '../Progress/Progress.svelte';
7
+ import { pauseAllConfirmationTimers } from './timers';
8
+ import type { HITLAction, HITLProperties, HITLResponse, HITLSection } from './properties';
9
+
10
+ /**
11
+ * Human-in-the-loop approval card: the assistant wants to run an action and the
12
+ * person approves or cancels it. Unless disabled, a countdown sweeps across the
13
+ * confirm button and auto-approves when it completes; interacting with any
14
+ * HITL pauses every sibling's countdown.
15
+ */
16
+ let {
17
+ confirmationId,
18
+ title,
19
+ description,
20
+ sections,
21
+ functionArguments,
22
+ hiddenKeys,
23
+ onConfirm,
24
+ confirmLabel = 'Confirm',
25
+ cancelLabel = 'Cancel',
26
+ countdownSeconds = 10,
27
+ autoCancelSeconds = 0,
28
+ isMicMuted = false,
29
+ onMicToggle = null,
30
+ isHistoryMode = false,
31
+ initialState = null,
32
+ approvedIcon,
33
+ rejectedIcon,
34
+ badgeLabel = 'ACTION',
35
+ approvedLabel = 'Approved',
36
+ autoApprovedLabel = 'Completed',
37
+ rejectedLabel = 'Action halted',
38
+ expiredLabel = 'Action timed out',
39
+ testId,
40
+ confirmTestId,
41
+ cancelTestId,
42
+ completionTestId,
43
+ completionTextTestId,
44
+ classes
45
+ }: HITLProperties = $props();
46
+
47
+ const DEFAULT_HIDDEN_KEYS = [
48
+ 'action',
49
+ 'userhasconfirmed',
50
+ 'confirmed',
51
+ 'userid',
52
+ 'sessionid',
53
+ 'timestamp'
54
+ ];
55
+
56
+ let isProcessing = $state(false);
57
+ let localResponse = $state<HITLResponse | null>(null);
58
+ let countdownActive = $state(false);
59
+ // Seeded from the prop; startCountdown re-seeds it on every (re)start.
60
+ // svelte-ignore state_referenced_locally
61
+ let timeRemaining = $state(countdownSeconds);
62
+ let countdownInterval: ReturnType<typeof setInterval> | null = null;
63
+ let autoCancelTimeout: ReturnType<typeof setTimeout> | null = null;
64
+ let originalMicState: boolean | null = null;
65
+
66
+ // History mode renders the settled state from props; live mode from local state.
67
+ // A history card with no initialState renders as expired rather than falling
68
+ // through to the pending interactive card — a replayed card must never be able
69
+ // to fire onConfirm.
70
+ const userResponse = $derived.by((): HITLResponse | null => {
71
+ if (isHistoryMode) {
72
+ if (initialState === null) {
73
+ return 'expired';
74
+ }
75
+ if (initialState.status === 'EXPIRED') {
76
+ return 'expired';
77
+ }
78
+ return initialState.approved === true ? 'approved' : 'rejected';
79
+ }
80
+ return localResponse;
81
+ });
82
+ const isCompleted = $derived(userResponse !== null);
83
+ const isApproved = $derived(userResponse === 'approved' || userResponse === 'auto-approved');
84
+ const elapsedTime = $derived(countdownSeconds - timeRemaining);
85
+
86
+ const completionText = $derived(
87
+ userResponse === 'auto-approved'
88
+ ? autoApprovedLabel
89
+ : userResponse === 'approved'
90
+ ? approvedLabel
91
+ : userResponse === 'expired'
92
+ ? expiredLabel
93
+ : rejectedLabel
94
+ );
95
+
96
+ const stopCountdown = (): void => {
97
+ if (countdownInterval !== null) {
98
+ clearInterval(countdownInterval);
99
+ countdownInterval = null;
100
+ }
101
+ countdownActive = false;
102
+ };
103
+
104
+ const clearAutoCancel = (): void => {
105
+ if (autoCancelTimeout !== null) {
106
+ clearTimeout(autoCancelTimeout);
107
+ autoCancelTimeout = null;
108
+ }
109
+ };
110
+
111
+ const restoreMicState = async (): Promise<void> => {
112
+ if (onMicToggle !== null && originalMicState !== null && isMicMuted !== originalMicState) {
113
+ try {
114
+ await onMicToggle();
115
+ } catch {
116
+ // Mic restoration is best-effort; a failure must not block the decision.
117
+ }
118
+ }
119
+ };
120
+
121
+ // Latched by the first decision path (click, countdown, auto-cancel) BEFORE its
122
+ // await, so a second path cannot also run restoreMicState — two restorations
123
+ // toggle the mic back to the wrong state, since the isMicMuted prop cannot have
124
+ // updated in between.
125
+ let decisionPending = false;
126
+
127
+ const settle = (action: HITLAction): void => {
128
+ if (isProcessing || isCompleted) {
129
+ return;
130
+ }
131
+ localResponse = action;
132
+ isProcessing = true;
133
+ try {
134
+ onConfirm?.({
135
+ confirmationId,
136
+ action,
137
+ approved: action !== 'rejected'
138
+ });
139
+ } catch {
140
+ // The decision failed to hand off; release the card for another attempt.
141
+ localResponse = null;
142
+ decisionPending = false;
143
+ } finally {
144
+ isProcessing = false;
145
+ }
146
+ };
147
+
148
+ const decide = async (action: HITLAction): Promise<void> => {
149
+ if (decisionPending || isCompleted) {
150
+ return;
151
+ }
152
+ decisionPending = true;
153
+ await restoreMicState();
154
+ settle(action);
155
+ };
156
+
157
+ const startCountdown = (): void => {
158
+ countdownActive = true;
159
+ timeRemaining = countdownSeconds;
160
+ countdownInterval = setInterval(() => {
161
+ // A sibling card's interaction pauses this countdown on its next tick.
162
+ if (get(pauseAllConfirmationTimers)) {
163
+ stopCountdown();
164
+ return;
165
+ }
166
+ timeRemaining = timeRemaining - 0.1;
167
+ if (timeRemaining <= 0) {
168
+ stopCountdown();
169
+ void decide('auto-approved');
170
+ }
171
+ }, 100);
172
+ };
173
+
174
+ const interact = async (action: HITLAction): Promise<void> => {
175
+ pauseAllConfirmationTimers.set(true);
176
+ stopCountdown();
177
+ clearAutoCancel();
178
+ await decide(action);
179
+ };
180
+
181
+ onMount(() => {
182
+ if (isHistoryMode) {
183
+ return;
184
+ }
185
+ if (onMicToggle !== null) {
186
+ originalMicState = isMicMuted;
187
+ if (!isMicMuted) {
188
+ void Promise.resolve(onMicToggle()).catch(() => {
189
+ // Auto-mute is best-effort.
190
+ });
191
+ }
192
+ }
193
+ pauseAllConfirmationTimers.set(false);
194
+ if (countdownSeconds > 0) {
195
+ startCountdown();
196
+ } else if (autoCancelSeconds > 0) {
197
+ autoCancelTimeout = setTimeout(() => {
198
+ void decide('rejected');
199
+ }, autoCancelSeconds * 1000);
200
+ }
201
+ });
202
+
203
+ onDestroy(() => {
204
+ stopCountdown();
205
+ clearAutoCancel();
206
+ });
207
+
208
+ const titleCase = (key: string): string => {
209
+ return key
210
+ .replace(/([A-Z])/g, ' $1')
211
+ .trim()
212
+ .toLowerCase()
213
+ .replace(/\b\w/g, (character) => character.toUpperCase());
214
+ };
215
+
216
+ const formatValue = (value: unknown, indentLevel: number): string => {
217
+ const indent = ' '.repeat(indentLevel);
218
+ if (value === null) {
219
+ return '';
220
+ }
221
+ if (typeof value === 'string') {
222
+ const trimmed = value.trim();
223
+ if (!trimmed || trimmed === '-') {
224
+ return '';
225
+ }
226
+ if (trimmed === '*') {
227
+ return 'All';
228
+ }
229
+ return trimmed.replace(/\s+/g, ' ');
230
+ }
231
+ if (typeof value === 'number') {
232
+ return value.toString();
233
+ }
234
+ if (typeof value === 'boolean') {
235
+ return value ? 'Yes' : 'No';
236
+ }
237
+ if (Array.isArray(value)) {
238
+ return value
239
+ .map((item) => {
240
+ if (typeof item === 'object' && item !== null) {
241
+ return formatValue(item, indentLevel + 1);
242
+ }
243
+ return `${indent}• ${formatValue(item, 0)}`;
244
+ })
245
+ .filter((line) => line)
246
+ .join('\n');
247
+ }
248
+ if (typeof value === 'object') {
249
+ const entries = Object.entries(value);
250
+ if (entries.length === 0) {
251
+ return '{}';
252
+ }
253
+ return entries
254
+ .map(([key, nested]) => {
255
+ const readableKey = titleCase(String(key));
256
+ if (typeof nested === 'object' && nested !== null && !Array.isArray(nested)) {
257
+ return `${indent}${readableKey}:\n${formatValue(nested, indentLevel + 1)}`;
258
+ }
259
+ const formatted = formatValue(nested, indentLevel + 1);
260
+ return formatted ? `${indent}${readableKey}: ${formatted}` : '';
261
+ })
262
+ .filter((line) => line)
263
+ .join('\n');
264
+ }
265
+ return JSON.stringify(value);
266
+ };
267
+
268
+ const formatArguments = (args: Record<string, unknown>): HITLSection[] => {
269
+ const hidden = (hiddenKeys ?? DEFAULT_HIDDEN_KEYS).map((key) => key.toLowerCase());
270
+ const built: HITLSection[] = [];
271
+ for (const [key, value] of Object.entries(args)) {
272
+ if (hidden.includes(key.toLowerCase()) || value === null) {
273
+ continue;
274
+ }
275
+ const wildcardKey = key
276
+ .replace(/([A-Z])/g, ' $1')
277
+ .trim()
278
+ .replace(/\btype\b/gi, '')
279
+ .trim();
280
+ const formatted =
281
+ typeof value === 'string' && value.trim() === '*'
282
+ ? `All ${wildcardKey}s`
283
+ : formatValue(value, 0);
284
+ if (formatted.trim()) {
285
+ built.push({ label: key.replace(/([A-Z])/g, ' $1').trim(), value: formatted });
286
+ }
287
+ }
288
+ return built.length > 0 ? built : [{ label: 'PARAMETERS', value: 'No parameters' }];
289
+ };
290
+
291
+ const parameterSections = $derived.by((): HITLSection[] => {
292
+ if (sections && sections.length > 0) {
293
+ return sections;
294
+ }
295
+ if (functionArguments && Object.keys(functionArguments).length > 0) {
296
+ return formatArguments(functionArguments);
297
+ }
298
+ return [{ label: 'PARAMETERS', value: 'No parameters' }];
299
+ });
300
+ </script>
301
+
302
+ <div
303
+ class="hitl {classes ?? ''}"
304
+ data-confirmation-id={confirmationId}
305
+ data-pw={typeof testId === 'string' ? testId : null}
306
+ testID={typeof testId === 'string' ? testId : null}
307
+ >
308
+ <Card
309
+ cssVars={{
310
+ '--card-background': 'var(--hitl-background, #ffffff)',
311
+ '--card-border': 'var(--hitl-border, 1px solid #e4e4e7)',
312
+ '--card-border-radius': 'var(--hitl-border-radius, 0.5rem)',
313
+ '--card-content-padding': 'var(--hitl-padding, 1.25rem)',
314
+ '--card-overflow': 'visible',
315
+ '--card-width': '100%'
316
+ }}
317
+ >
318
+ <div class="confirmation-header">
319
+ <span class="badge">{badgeLabel}</span>
320
+ <span class="title" data-pw={testId && `${testId}-title`}>{title}</span>
321
+ </div>
322
+
323
+ <div class="header-border"></div>
324
+
325
+ {#if typeof description === 'string' && description.length > 0}
326
+ <p class="description" data-pw={testId && `${testId}-description`}>{description}</p>
327
+ {/if}
328
+
329
+ <div class="confirmation-body">
330
+ {#each parameterSections as section, sectionIndex (sectionIndex)}
331
+ <div class="params">
332
+ <span class="parameter-label">{section.label}</span>
333
+ <span class="parameter-value">{section.value}</span>
334
+ </div>
335
+ {/each}
336
+
337
+ {#if isCompleted && userResponse !== null}
338
+ <div
339
+ class="completion"
340
+ data-pw={completionTestId ?? (testId && `${testId}-completion`) ?? null}
341
+ >
342
+ <span class="completion-icon" class:halted={!isApproved} aria-hidden="true">
343
+ {#if isApproved}
344
+ {#if approvedIcon}
345
+ {@render approvedIcon()}
346
+ {:else}
347
+ <svg viewBox="0 0 20 20" fill="none">
348
+ <circle cx="10" cy="10" r="9" stroke="currentColor" stroke-width="1.5" />
349
+ <path
350
+ d="M6 10.2l2.6 2.6L14 7.4"
351
+ stroke="currentColor"
352
+ stroke-width="1.5"
353
+ stroke-linecap="round"
354
+ stroke-linejoin="round"
355
+ />
356
+ </svg>
357
+ {/if}
358
+ {:else if rejectedIcon}
359
+ {@render rejectedIcon()}
360
+ {:else}
361
+ <svg viewBox="0 0 20 20" fill="none">
362
+ <circle cx="10" cy="10" r="9" stroke="currentColor" stroke-width="1.5" />
363
+ <path d="M6 10h8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
364
+ </svg>
365
+ {/if}
366
+ </span>
367
+ <span
368
+ class="completion-text"
369
+ class:halted={!isApproved}
370
+ data-pw={completionTextTestId ?? (testId && `${testId}-completion-text`) ?? null}
371
+ >
372
+ {completionText}
373
+ </span>
374
+ </div>
375
+ {:else}
376
+ <div class="action-buttons">
377
+ <div class="cancel-button">
378
+ <Button
379
+ variant="secondary"
380
+ text={cancelLabel}
381
+ enable={!isProcessing}
382
+ testId={cancelTestId ?? (testId && `${testId}-cancel`)}
383
+ onclick={() => interact('rejected')}
384
+ />
385
+ </div>
386
+ <div class="confirm-button">
387
+ {#if countdownActive}
388
+ <div class="progress-anchor">
389
+ <Progress value={elapsedTime} max={countdownSeconds} />
390
+ </div>
391
+ {/if}
392
+ <Button
393
+ text={confirmLabel}
394
+ enable={!isProcessing}
395
+ testId={confirmTestId ?? (testId && `${testId}-confirm`)}
396
+ onclick={() => interact('approved')}
397
+ />
398
+ </div>
399
+ </div>
400
+ {/if}
401
+ </div>
402
+ </Card>
403
+ </div>
404
+
405
+ <style>
406
+ .hitl {
407
+ position: relative;
408
+ width: 100%;
409
+ max-width: var(--hitl-max-width, 100%);
410
+ margin: var(--hitl-margin, 0);
411
+ animation: hitl-slide-in 0.3s ease-out forwards;
412
+ contain: layout style;
413
+ }
414
+
415
+ .confirmation-header {
416
+ display: flex;
417
+ flex-direction: column;
418
+ gap: var(--hitl-header-gap, 2px);
419
+ margin-bottom: var(--hitl-header-margin-bottom, 0.75rem);
420
+ }
421
+
422
+ .badge {
423
+ font-size: var(--hitl-badge-font-size, 0.6875rem);
424
+ font-weight: var(--hitl-badge-font-weight, 600);
425
+ letter-spacing: var(--hitl-badge-letter-spacing, 0.06em);
426
+ color: var(--hitl-badge-color, #858585);
427
+ text-transform: uppercase;
428
+ }
429
+
430
+ .title {
431
+ font-size: var(--hitl-title-font-size, 1rem);
432
+ font-weight: var(--hitl-title-font-weight, 600);
433
+ color: var(--hitl-title-color, #1f1f23);
434
+ }
435
+
436
+ .header-border {
437
+ height: 1px;
438
+ background-color: var(--hitl-divider-color, #e4e4e7);
439
+ }
440
+
441
+ .description {
442
+ margin: 0;
443
+ padding: var(--hitl-description-padding, 0.25rem 0);
444
+ font-size: var(--hitl-description-font-size, 0.8125rem);
445
+ line-height: var(--hitl-description-line-height, inherit);
446
+ color: var(--hitl-description-color, #858585);
447
+ }
448
+
449
+ .confirmation-body {
450
+ display: flex;
451
+ flex-direction: column;
452
+ gap: var(--hitl-content-gap, 1rem);
453
+ margin-top: var(--hitl-content-margin-top, 0.75rem);
454
+ }
455
+
456
+ .params {
457
+ display: flex;
458
+ flex-direction: column;
459
+ gap: var(--hitl-param-gap, 0.25rem);
460
+ }
461
+
462
+ .parameter-label {
463
+ font-size: var(--hitl-param-label-font-size, 0.6875rem);
464
+ font-weight: var(--hitl-param-label-font-weight, 600);
465
+ letter-spacing: var(--hitl-param-label-letter-spacing, 0.04em);
466
+ color: var(--hitl-param-label-color, #858585);
467
+ word-spacing: var(--hitl-param-label-word-spacing, normal);
468
+ text-transform: uppercase;
469
+ }
470
+
471
+ .parameter-value {
472
+ white-space: pre-line;
473
+ overflow-wrap: break-word;
474
+ font-size: var(--hitl-param-value-font-size, 0.875rem);
475
+ line-height: var(--hitl-param-value-line-height, 1.4);
476
+ color: var(--hitl-param-value-color, #1f1f23);
477
+ word-spacing: var(--hitl-param-value-word-spacing, normal);
478
+ text-transform: capitalize;
479
+ }
480
+
481
+ .action-buttons {
482
+ display: flex;
483
+ gap: var(--hitl-buttons-gap, 0.75rem);
484
+ width: 100%;
485
+ }
486
+
487
+ .cancel-button,
488
+ .confirm-button {
489
+ flex: 1;
490
+ position: relative;
491
+ --button-width: 100%;
492
+ }
493
+
494
+ /* The countdown sweep: a transparent Progress stretched over the confirm
495
+ button whose bar darkens what is underneath as it advances. */
496
+ .progress-anchor {
497
+ position: absolute;
498
+ inset: 0;
499
+ z-index: 1;
500
+ pointer-events: none;
501
+ border-radius: var(--hitl-border-radius, 0.5rem);
502
+ overflow: hidden;
503
+ --progress-track-background: transparent;
504
+ --progress-bar-background: transparent;
505
+ --progress-bar-transition: width 0.1s linear;
506
+ --progress-track-height: 100%;
507
+ --progress-container-padding: 0;
508
+ }
509
+
510
+ .progress-anchor > :global(*) {
511
+ height: 100%;
512
+ }
513
+
514
+ .progress-anchor :global(.bar) {
515
+ backdrop-filter: var(--hitl-countdown-filter, brightness(0.8));
516
+ }
517
+
518
+ .completion {
519
+ display: flex;
520
+ align-items: center;
521
+ justify-content: center;
522
+ gap: var(--hitl-completion-gap, 0.5rem);
523
+ background: var(--hitl-completion-background, #f4f4f5);
524
+ border-radius: var(--hitl-border-radius, 0.5rem);
525
+ padding: var(--hitl-completion-padding, 1rem);
526
+ animation: hitl-fade-in 0.3s ease-in-out;
527
+ }
528
+
529
+ .completion-icon {
530
+ display: inline-flex;
531
+ width: var(--hitl-completion-icon-size, 1.25rem);
532
+ height: var(--hitl-completion-icon-size, 1.25rem);
533
+ color: var(--hitl-approved-color, #16a34a);
534
+ }
535
+
536
+ .completion-icon svg {
537
+ width: 100%;
538
+ height: 100%;
539
+ }
540
+
541
+ .completion-icon.halted {
542
+ color: var(--hitl-halted-color, #b45309);
543
+ }
544
+
545
+ .completion-text {
546
+ font-size: var(--hitl-completion-font-size, 0.875rem);
547
+ font-weight: var(--hitl-completion-font-weight, 600);
548
+ color: var(--hitl-approved-color, #16a34a);
549
+ }
550
+
551
+ .completion-text.halted {
552
+ color: var(--hitl-halted-color, #b45309);
553
+ }
554
+
555
+ @keyframes hitl-slide-in {
556
+ from {
557
+ opacity: 0;
558
+ transform: translateY(8px);
559
+ }
560
+
561
+ to {
562
+ opacity: 1;
563
+ transform: translateY(0);
564
+ }
565
+ }
566
+
567
+ @keyframes hitl-fade-in {
568
+ from {
569
+ opacity: 0;
570
+ }
571
+
572
+ to {
573
+ opacity: 1;
574
+ }
575
+ }
576
+ </style>
@@ -0,0 +1,4 @@
1
+ import type { HITLProperties } from './properties';
2
+ declare const HITL: import("svelte").Component<HITLProperties, {}, "">;
3
+ type HITL = ReturnType<typeof HITL>;
4
+ export default HITL;
@@ -0,0 +1,71 @@
1
+ import type { Snippet } from 'svelte';
2
+ export type HITLAction = 'approved' | 'rejected' | 'auto-approved';
3
+ export type HITLResponse = HITLAction | 'expired';
4
+ export type HITLSection = {
5
+ label: string;
6
+ value: string;
7
+ };
8
+ export type HITLEvent = {
9
+ confirmationId: string;
10
+ action: HITLAction;
11
+ approved: boolean;
12
+ };
13
+ export type HITLInitialState = {
14
+ approved?: boolean;
15
+ /** `'EXPIRED'` renders the timed-out completion state. */
16
+ status?: string;
17
+ };
18
+ export type HITLProperties = OptionalHITLProperties & MandatoryHITLProperties;
19
+ export type MandatoryHITLProperties = {
20
+ confirmationId: string;
21
+ /** The action being approved, already humanised — e.g. "Create discount". */
22
+ title: string;
23
+ };
24
+ export type OptionalHITLProperties = {
25
+ description?: string;
26
+ /**
27
+ * Labelled parameter blocks to show. When omitted, `functionArguments` is
28
+ * formatted generically instead.
29
+ */
30
+ sections?: HITLSection[];
31
+ /** Raw arguments, formatted generically when no `sections` are given. */
32
+ functionArguments?: Record<string, unknown>;
33
+ /** Argument keys (case-insensitive) hidden from the generic formatting. */
34
+ hiddenKeys?: string[];
35
+ onConfirm?: (event: HITLEvent) => void;
36
+ confirmLabel?: string;
37
+ cancelLabel?: string;
38
+ /**
39
+ * Seconds until the card auto-approves, with a sweep across the confirm
40
+ * button. `0` disables auto-approval.
41
+ */
42
+ countdownSeconds?: number;
43
+ /**
44
+ * Seconds until an untouched card auto-rejects — for confirmations that must
45
+ * not auto-approve (e.g. OAuth) but should not block a conversation forever.
46
+ */
47
+ autoCancelSeconds?: number;
48
+ /** Current mic state — muted while the card is open, restored on decision. */
49
+ isMicMuted?: boolean;
50
+ onMicToggle?: (() => void | Promise<void>) | null;
51
+ /** Renders a settled card from `initialState` with no timers or buttons. */
52
+ isHistoryMode?: boolean;
53
+ initialState?: HITLInitialState | null;
54
+ approvedIcon?: Snippet;
55
+ rejectedIcon?: Snippet;
56
+ badgeLabel?: string;
57
+ approvedLabel?: string;
58
+ autoApprovedLabel?: string;
59
+ rejectedLabel?: string;
60
+ expiredLabel?: string;
61
+ testId?: string;
62
+ /** Override the confirm button's test id (default: `<testId>-confirm`). */
63
+ confirmTestId?: string;
64
+ /** Override the cancel button's test id (default: `<testId>-cancel`). */
65
+ cancelTestId?: string;
66
+ /** Override the completion strip's test id (default: `<testId>-completion`). */
67
+ completionTestId?: string;
68
+ /** Override the completion text's test id (default: `<testId>-completion-text`). */
69
+ completionTextTestId?: string;
70
+ classes?: string;
71
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Shared across every mounted HITL. When the user interacts with one
3
+ * card, all sibling cards stop their countdowns so an auto-approval never fires
4
+ * while the user is actively deciding.
5
+ */
6
+ export declare const pauseAllConfirmationTimers: import("svelte/store").Writable<boolean>;
@@ -0,0 +1,7 @@
1
+ import { writable } from 'svelte/store';
2
+ /**
3
+ * Shared across every mounted HITL. When the user interacts with one
4
+ * card, all sibling cards stop their countdowns so an auto-approval never fires
5
+ * while the user is actively deciding.
6
+ */
7
+ export const pauseAllConfirmationTimers = writable(false);
@@ -81,6 +81,12 @@
81
81
  .pill {
82
82
  display: inline-flex;
83
83
  align-items: center;
84
+ /* A pill is content-width by default. A caller stacking pills into a menu needs
85
+ them to fill the column and align their labels left, which is a layout choice
86
+ the call site owns — hence tokens rather than a variant. */
87
+ width: var(--pill-width, auto);
88
+ justify-content: var(--pill-justify-content, center);
89
+ text-align: var(--pill-text-align, center);
84
90
  gap: var(--pill-gap, 4px);
85
91
  background-color: var(--pill-background, #e0e0e0);
86
92
  color: var(--pill-color, #333333);