@juspay/svelte-ui-components 4.52.0 → 4.53.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/AttachmentChipRow/AttachmentChipRow.svelte +3 -2
- package/dist/AttachmentChipRow/properties.d.ts +2 -0
- package/dist/Book/Book.svelte +2 -0
- package/dist/Book/properties.d.ts +2 -0
- package/dist/Chat/Chat.svelte +2 -0
- package/dist/Chat/properties.d.ts +2 -0
- package/dist/Chat/types.d.ts +2 -0
- package/dist/ChatMessage/ChatMessage.svelte +18 -3
- package/dist/ChatMessage/properties.d.ts +6 -0
- package/dist/ChatMessageList/ChatMessageList.svelte +31 -7
- package/dist/ChatMessageList/properties.d.ts +2 -0
- package/dist/Choicebox/Choicebox.svelte +33 -23
- package/dist/Draggable/Draggable.svelte +4 -2
- package/dist/HITL/HITL.svelte +52 -11
- package/dist/HITL/properties.d.ts +5 -0
- package/dist/MediaPlayer/MediaPlayer.svelte +28 -13
- package/dist/MediaPlayer/properties.d.ts +2 -0
- package/dist/Modal/Modal.svelte +30 -43
- package/dist/Modal/properties.d.ts +5 -4
- package/dist/Pagination/Pagination.svelte +2 -0
- package/dist/Pagination/properties.d.ts +2 -0
- package/dist/Pill/Pill.svelte +61 -4
- package/dist/Pill/properties.d.ts +8 -16
- package/dist/Radio/Radio.svelte +6 -2
- package/dist/RatingGroup/RatingGroup.svelte +38 -26
- package/dist/Select/Select.svelte +11 -3
- package/dist/StatCard/StatCard.svelte +68 -17
- package/dist/StatCard/properties.d.ts +1 -1
- package/dist/Stepper/Step.svelte +2 -2
- package/dist/Table/Table.svelte +32 -2
- package/dist/Table/properties.d.ts +11 -5
- package/dist/Tabs/Tabs.svelte +187 -12
- package/dist/Toast/Toast.svelte +42 -12
- package/dist/_chart/LabelPlate.svelte +3 -1
- package/dist/styles/theme-dark.css +3 -0
- package/dist/utils.js +20 -2
- package/dist-codemod/codemod/cli.js +4 -4
- package/dist-wc/index.js +3203 -2794
- package/package.json +1 -1
- package/scripts/postinstall.mjs +13 -15
|
@@ -26,14 +26,15 @@
|
|
|
26
26
|
removeIcon,
|
|
27
27
|
fileIcon,
|
|
28
28
|
testId,
|
|
29
|
-
classes
|
|
29
|
+
classes,
|
|
30
|
+
ariaLabel = 'Pending attachments'
|
|
30
31
|
}: AttachmentChipRowProperties = $props();
|
|
31
32
|
</script>
|
|
32
33
|
|
|
33
34
|
{#if images.length > 0 || videos.length > 0 || files.length > 0}
|
|
34
35
|
<Scroller
|
|
35
36
|
direction="horizontal"
|
|
36
|
-
ariaLabel
|
|
37
|
+
{ariaLabel}
|
|
37
38
|
showArrows={false}
|
|
38
39
|
hideScrollbar={false}
|
|
39
40
|
showGradient={false}
|
package/dist/Book/Book.svelte
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
showNavigation = true,
|
|
12
12
|
showPageIndicator = true,
|
|
13
13
|
enableSwipe = false,
|
|
14
|
+
ariaLabel = 'Book',
|
|
14
15
|
testId,
|
|
15
16
|
previousIcon,
|
|
16
17
|
nextIcon,
|
|
@@ -108,6 +109,7 @@
|
|
|
108
109
|
onmousedown={handleMouseDown}
|
|
109
110
|
onmouseup={handleMouseUp}
|
|
110
111
|
role="region"
|
|
112
|
+
aria-label={ariaLabel}
|
|
111
113
|
tabindex="0"
|
|
112
114
|
>
|
|
113
115
|
<div class="book-viewport">
|
|
@@ -14,6 +14,8 @@ export type OptionalBookProperties = {
|
|
|
14
14
|
showNavigation?: boolean;
|
|
15
15
|
showPageIndicator?: boolean;
|
|
16
16
|
enableSwipe?: boolean;
|
|
17
|
+
/** Accessible name for the keyboard-operated book region. Defaults to "Book". */
|
|
18
|
+
ariaLabel?: string;
|
|
17
19
|
testId?: string;
|
|
18
20
|
previousIcon?: Snippet;
|
|
19
21
|
nextIcon?: Snippet;
|
package/dist/Chat/Chat.svelte
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
hideScrollbar = false,
|
|
24
24
|
jump = true,
|
|
25
25
|
jumpLabel = 'Jump to latest',
|
|
26
|
+
messageListLabel,
|
|
26
27
|
jumpIcon,
|
|
27
28
|
toolStatus = null,
|
|
28
29
|
suggestions = [],
|
|
@@ -97,6 +98,7 @@
|
|
|
97
98
|
{/if}
|
|
98
99
|
|
|
99
100
|
<ChatMessageList
|
|
101
|
+
ariaLabel={messageListLabel ?? (title.trim() ? `${title} messages` : 'Conversation messages')}
|
|
100
102
|
{messages}
|
|
101
103
|
{autoscroll}
|
|
102
104
|
{scrollPolicy}
|
|
@@ -23,6 +23,8 @@ export type OptionalChatProperties = {
|
|
|
23
23
|
hideScrollbar?: ChatMessageListProperties['hideScrollbar'];
|
|
24
24
|
jump?: ChatMessageListProperties['jump'];
|
|
25
25
|
jumpLabel?: ChatMessageListProperties['jumpLabel'];
|
|
26
|
+
/** Transcript name; defaults to the chat title plus `messages`, or `Conversation messages`. */
|
|
27
|
+
messageListLabel?: ChatMessageListProperties['ariaLabel'];
|
|
26
28
|
jumpIcon?: ChatMessageListProperties['jumpIcon'];
|
|
27
29
|
toolStatus?: ChatToolStatus | null;
|
|
28
30
|
suggestions?: ChatSuggestion[];
|
package/dist/Chat/types.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export type ChatMessageData = {
|
|
|
11
11
|
* MarkdownText). Wins over `html` and `content` in the bubble.
|
|
12
12
|
*/
|
|
13
13
|
markdown?: string;
|
|
14
|
+
/** Accessible table scroll-region name for static and streamed markdown bubbles. */
|
|
15
|
+
markdownTableLabel?: string;
|
|
14
16
|
streaming?: boolean;
|
|
15
17
|
/**
|
|
16
18
|
* Reveal this message progressively as it grows. Opt-in per message so it cannot stack with
|
|
@@ -10,12 +10,14 @@
|
|
|
10
10
|
import { onDestroy, onMount } from 'svelte';
|
|
11
11
|
import { partyOf } from '../Chat/roles';
|
|
12
12
|
import type { ChatMessageProperties } from './properties';
|
|
13
|
+
import type { RenderMarkdownOptions } from '../MarkdownText/properties';
|
|
13
14
|
|
|
14
15
|
let {
|
|
15
16
|
role,
|
|
16
17
|
content = '',
|
|
17
18
|
html,
|
|
18
19
|
markdown,
|
|
20
|
+
markdownTableLabel,
|
|
19
21
|
body,
|
|
20
22
|
streaming = false,
|
|
21
23
|
typewriter = false,
|
|
@@ -90,7 +92,9 @@
|
|
|
90
92
|
`marked` — a peer dependency — is only pulled in at runtime, never during
|
|
91
93
|
SSR, and rendering `markdown` degrades to the `html`/`content` fallback
|
|
92
94
|
until the module resolves or when the peer is absent. */
|
|
93
|
-
let renderMarkdownFn = $state<
|
|
95
|
+
let renderMarkdownFn = $state<
|
|
96
|
+
((source: string, options?: RenderMarkdownOptions) => string) | null
|
|
97
|
+
>(null);
|
|
94
98
|
|
|
95
99
|
onMount(() => {
|
|
96
100
|
void import('../MarkdownText/markdown')
|
|
@@ -102,8 +106,19 @@
|
|
|
102
106
|
});
|
|
103
107
|
});
|
|
104
108
|
|
|
109
|
+
// Both rendering branches share the same options-aware renderer. Capture the label
|
|
110
|
+
// reactively so a locale/name update also refreshes already-revealed typewriter HTML.
|
|
111
|
+
let messageMarkdownRenderer = $derived.by(() => {
|
|
112
|
+
const renderer = renderMarkdownFn;
|
|
113
|
+
if (renderer === null) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
const options: RenderMarkdownOptions =
|
|
117
|
+
typeof markdownTableLabel === 'string' ? { tableLabel: markdownTableLabel } : {};
|
|
118
|
+
return (source: string) => renderer(source, options);
|
|
119
|
+
});
|
|
105
120
|
let effectiveHtml = $derived(
|
|
106
|
-
hasMarkdown &&
|
|
121
|
+
hasMarkdown && messageMarkdownRenderer !== null ? messageMarkdownRenderer(markdown ?? '') : html
|
|
107
122
|
);
|
|
108
123
|
let hasHtml = $derived(typeof effectiveHtml === 'string' && effectiveHtml.length > 0);
|
|
109
124
|
|
|
@@ -128,7 +143,7 @@
|
|
|
128
143
|
(!hasMarkdown || renderMarkdownFn !== null)
|
|
129
144
|
);
|
|
130
145
|
let typewriterRenderText = $derived(
|
|
131
|
-
hasMarkdown &&
|
|
146
|
+
hasMarkdown && messageMarkdownRenderer !== null ? { renderText: messageMarkdownRenderer } : {}
|
|
132
147
|
);
|
|
133
148
|
// Clamped at the boundary the way LoadingDots clamps `dots`, so the prop's floor is part
|
|
134
149
|
// of this component's contract rather than something a consumer discovers. The finite
|
|
@@ -20,6 +20,12 @@ export type OptionalChatMessageProperties = {
|
|
|
20
20
|
* (for server-rendered markdown use `MarkdownText` or `renderMarkdown`).
|
|
21
21
|
*/
|
|
22
22
|
markdown?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Accessible name for each table scroll region rendered from `markdown`, including
|
|
25
|
+
* progressive/typewriter rendering. Omitted or empty leaves the shared renderer's
|
|
26
|
+
* unnamed, keyboard-scrollable wrapper unchanged. Does not alter `html` or `body`.
|
|
27
|
+
*/
|
|
28
|
+
markdownTableLabel?: string;
|
|
23
29
|
/**
|
|
24
30
|
* Replaces the rendered bubble body with arbitrary markup while keeping the
|
|
25
31
|
* message chrome (role styling, avatar, header, attachments, copy/retry/
|
|
@@ -28,13 +28,15 @@
|
|
|
28
28
|
onretry,
|
|
29
29
|
onfeedback,
|
|
30
30
|
testId,
|
|
31
|
-
classes
|
|
31
|
+
classes,
|
|
32
|
+
ariaLabel = 'Conversation messages'
|
|
32
33
|
}: ChatMessageListProperties = $props();
|
|
33
34
|
|
|
34
35
|
let listEl: HTMLElement | null = $state(null);
|
|
35
36
|
let innerEl: HTMLElement | null = $state(null);
|
|
36
37
|
let atBottom = $state(true);
|
|
37
38
|
let scrollable = $state(false);
|
|
39
|
+
let keyboardScrollable = $state(false);
|
|
38
40
|
let pinActive = false;
|
|
39
41
|
// Set when a turn without pinHold has asked for its reservation back, but the reply is not yet
|
|
40
42
|
// tall enough to hold the pinned position without it. Cleared by tryReleasePin.
|
|
@@ -78,6 +80,7 @@
|
|
|
78
80
|
|
|
79
81
|
function reportScrollState(node: HTMLElement): void {
|
|
80
82
|
atBottom = isNearBottom(node);
|
|
83
|
+
keyboardScrollable = node.scrollHeight - node.clientHeight > 1;
|
|
81
84
|
scrollable = node.scrollHeight - node.clientHeight > NEAR_BOTTOM_THRESHOLD;
|
|
82
85
|
onscrollstate?.({ atBottom, scrollable });
|
|
83
86
|
}
|
|
@@ -260,19 +263,29 @@
|
|
|
260
263
|
if (typeof ResizeObserver === 'undefined') {
|
|
261
264
|
return;
|
|
262
265
|
}
|
|
266
|
+
let pendingFrame: number | null = null;
|
|
263
267
|
const observer = new ResizeObserver(() => {
|
|
264
|
-
if (
|
|
268
|
+
if (pendingFrame !== null) {
|
|
265
269
|
return;
|
|
266
270
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
271
|
+
pendingFrame = requestAnimationFrame(() => {
|
|
272
|
+
pendingFrame = null;
|
|
273
|
+
if (listEl === null || !node.isConnected) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
if (scrollPolicy === 'near-bottom' && autoscroll && atBottom) {
|
|
277
|
+
listEl.scrollTop = listEl.scrollHeight;
|
|
278
|
+
}
|
|
279
|
+
tryReleasePin();
|
|
280
|
+
reportScrollState(listEl);
|
|
281
|
+
});
|
|
272
282
|
});
|
|
273
283
|
observer.observe(node);
|
|
274
284
|
return {
|
|
275
285
|
destroy(): void {
|
|
286
|
+
if (pendingFrame !== null) {
|
|
287
|
+
cancelAnimationFrame(pendingFrame);
|
|
288
|
+
}
|
|
276
289
|
observer.disconnect();
|
|
277
290
|
}
|
|
278
291
|
};
|
|
@@ -291,9 +304,14 @@
|
|
|
291
304
|
};
|
|
292
305
|
</script>
|
|
293
306
|
|
|
307
|
+
<!-- The transcript is an independent scrolling surface. Native keyboard focus
|
|
308
|
+
enables the browser's scroll keys while it overflows, even without actions. -->
|
|
309
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
294
310
|
<div
|
|
295
311
|
class="chat-message-list {classes ?? ''}"
|
|
296
312
|
role="log"
|
|
313
|
+
aria-label={ariaLabel}
|
|
314
|
+
tabindex={keyboardScrollable ? 0 : null}
|
|
297
315
|
aria-live="polite"
|
|
298
316
|
data-pw={typeof testId === 'string' ? testId : null}
|
|
299
317
|
testID={typeof testId === 'string' ? testId : null}
|
|
@@ -323,6 +341,7 @@
|
|
|
323
341
|
content={msg.content}
|
|
324
342
|
html={msg.html}
|
|
325
343
|
markdown={msg.markdown}
|
|
344
|
+
markdownTableLabel={msg.markdownTableLabel}
|
|
326
345
|
body={typeof messageBody === 'function' ? bodyFor : null}
|
|
327
346
|
streaming={msg.streaming}
|
|
328
347
|
typewriter={msg.typewriter}
|
|
@@ -352,6 +371,11 @@
|
|
|
352
371
|
</div>
|
|
353
372
|
|
|
354
373
|
<style>
|
|
374
|
+
.chat-message-list:focus-visible {
|
|
375
|
+
outline: var(--chat-message-list-focus-outline, 2px solid currentColor);
|
|
376
|
+
outline-offset: -2px;
|
|
377
|
+
}
|
|
378
|
+
|
|
355
379
|
.chat-message-list {
|
|
356
380
|
box-sizing: border-box;
|
|
357
381
|
display: flex;
|
|
@@ -44,6 +44,8 @@ export type OptionalChatMessageListProperties = {
|
|
|
44
44
|
allowCopy?: boolean;
|
|
45
45
|
testId?: string;
|
|
46
46
|
classes?: string;
|
|
47
|
+
/** Accessible transcript name. Default: `Conversation messages`. */
|
|
48
|
+
ariaLabel?: string;
|
|
47
49
|
};
|
|
48
50
|
export type ChatMessageListEventProperties = {
|
|
49
51
|
/**
|
|
@@ -82,12 +82,20 @@
|
|
|
82
82
|
onclick?.(selected);
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
function
|
|
89
|
-
|
|
90
|
-
|
|
85
|
+
// Let native validation choose the first invalid control. Redirect only its
|
|
86
|
+
// actual focus to the visible card, so checkValidity() stays a silent query
|
|
87
|
+
// and later invalid fields cannot replace the first field's focus.
|
|
88
|
+
function handleNativeFocus(event: FocusEvent & { currentTarget: HTMLInputElement }): void {
|
|
89
|
+
const carrier = event.currentTarget;
|
|
90
|
+
// Finish native interactive validation before moving focus. Firefox can
|
|
91
|
+
// open its validation UI after a synchronous handoff and swallow the next
|
|
92
|
+
// pointer click. A later owner or an unmounted card must keep its focus.
|
|
93
|
+
queueMicrotask(() => {
|
|
94
|
+
const root = carrier.getRootNode();
|
|
95
|
+
if (card?.isConnected && 'activeElement' in root && root.activeElement === carrier) {
|
|
96
|
+
card.focus();
|
|
97
|
+
}
|
|
98
|
+
});
|
|
91
99
|
}
|
|
92
100
|
|
|
93
101
|
function handleKeyDown(e: KeyboardEvent): void {
|
|
@@ -143,21 +151,6 @@
|
|
|
143
151
|
testID={testId}
|
|
144
152
|
{@attach groupMembership}
|
|
145
153
|
>
|
|
146
|
-
<input
|
|
147
|
-
type={mode === 'radio' ? 'radio' : 'checkbox'}
|
|
148
|
-
class="native-control"
|
|
149
|
-
checked={selected}
|
|
150
|
-
{disabled}
|
|
151
|
-
{value}
|
|
152
|
-
{required}
|
|
153
|
-
name={typeof name === 'string' ? name : null}
|
|
154
|
-
form={typeof form === 'string' ? form : null}
|
|
155
|
-
oninvalid={handleInvalid}
|
|
156
|
-
tabindex={-1}
|
|
157
|
-
aria-hidden="true"
|
|
158
|
-
data-pw={typeof testId === 'string' ? `${testId}-native-input` : null}
|
|
159
|
-
testID={typeof testId === 'string' ? `${testId}-native-input` : null}
|
|
160
|
-
/>
|
|
161
154
|
{#if typeof children === 'function'}
|
|
162
155
|
{@render children()}
|
|
163
156
|
{/if}
|
|
@@ -171,6 +164,23 @@
|
|
|
171
164
|
{/if}
|
|
172
165
|
</div>
|
|
173
166
|
|
|
167
|
+
<!-- Native form participation remains outside the interactive accessibility owner. -->
|
|
168
|
+
<input
|
|
169
|
+
type={mode === 'radio' ? 'radio' : 'checkbox'}
|
|
170
|
+
class="native-control"
|
|
171
|
+
checked={selected}
|
|
172
|
+
{disabled}
|
|
173
|
+
{value}
|
|
174
|
+
{required}
|
|
175
|
+
name={typeof name === 'string' ? name : null}
|
|
176
|
+
form={typeof form === 'string' ? form : null}
|
|
177
|
+
onfocus={handleNativeFocus}
|
|
178
|
+
tabindex={-1}
|
|
179
|
+
aria-hidden="true"
|
|
180
|
+
data-pw={typeof testId === 'string' ? `${testId}-native-input` : null}
|
|
181
|
+
testID={typeof testId === 'string' ? `${testId}-native-input` : null}
|
|
182
|
+
/>
|
|
183
|
+
|
|
174
184
|
{#if field.showsError}
|
|
175
185
|
<div
|
|
176
186
|
id={field.errorId}
|
|
@@ -197,8 +207,8 @@
|
|
|
197
207
|
.native-control {
|
|
198
208
|
position: absolute;
|
|
199
209
|
opacity: 0;
|
|
200
|
-
width:
|
|
201
|
-
height:
|
|
210
|
+
width: 1px;
|
|
211
|
+
height: 1px;
|
|
202
212
|
margin: 0;
|
|
203
213
|
pointer-events: none;
|
|
204
214
|
}
|
|
@@ -134,13 +134,15 @@
|
|
|
134
134
|
}
|
|
135
135
|
</script>
|
|
136
136
|
|
|
137
|
-
<!--
|
|
138
|
-
|
|
137
|
+
<!-- This named content group moves with arrow keys; it can contain independent
|
|
138
|
+
controls, so button semantics would hide their accessibility semantics. -->
|
|
139
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex, a11y_no_noninteractive_element_interactions -->
|
|
139
140
|
<div
|
|
140
141
|
class="draggable {classes ?? ''}"
|
|
141
142
|
class:dragging={active !== null}
|
|
142
143
|
data-pw={typeof testId === 'string' ? testId : null}
|
|
143
144
|
tabindex={disabled ? -1 : 0}
|
|
145
|
+
role="group"
|
|
144
146
|
aria-label={dragLabel}
|
|
145
147
|
style:transform={`translate(${x}px, ${y}px)`}
|
|
146
148
|
onpointerdown={startDrag}
|
package/dist/HITL/HITL.svelte
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { onMount, onDestroy } from 'svelte';
|
|
2
|
+
import { onMount, onDestroy, tick } from 'svelte';
|
|
3
3
|
import { get } from 'svelte/store';
|
|
4
4
|
import Button from '../Button/Button.svelte';
|
|
5
5
|
import Card from '../Card/Card.svelte';
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
showConfirm = true,
|
|
44
44
|
showEmptyParameters = true,
|
|
45
45
|
countdownSeconds = 10,
|
|
46
|
+
expiresAt,
|
|
46
47
|
autoCancelSeconds = 0,
|
|
47
48
|
isMicMuted = false,
|
|
48
49
|
// The resolver this replaced defaulted to null, and the call sites test for
|
|
@@ -88,9 +89,14 @@
|
|
|
88
89
|
// Seeded from the prop; startCountdown re-seeds it on every (re)start.
|
|
89
90
|
// svelte-ignore state_referenced_locally
|
|
90
91
|
let timeRemaining = $state(countdownSeconds);
|
|
92
|
+
// svelte-ignore state_referenced_locally
|
|
93
|
+
let countdownDuration = $state(countdownSeconds);
|
|
94
|
+
let relativeDeadline: number | null = null;
|
|
95
|
+
let countdownDeadline: number | null = null;
|
|
91
96
|
let countdownInterval: ReturnType<typeof setInterval> | null = null;
|
|
92
97
|
let autoCancelTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
93
98
|
let originalMicState: boolean | null = null;
|
|
99
|
+
let initialMicToggle: Promise<void> | null = null;
|
|
94
100
|
|
|
95
101
|
// Expiry from the caller takes precedence, including during a live request.
|
|
96
102
|
// History mode renders the settled state from props; live mode from local state.
|
|
@@ -111,7 +117,7 @@
|
|
|
111
117
|
});
|
|
112
118
|
const isCompleted = $derived(userResponse !== null);
|
|
113
119
|
const isApproved = $derived(userResponse === 'approved' || userResponse === 'auto-approved');
|
|
114
|
-
const elapsedTime = $derived(
|
|
120
|
+
const elapsedTime = $derived(Math.max(0, countdownDuration - timeRemaining));
|
|
115
121
|
const approvalBlocked = (action: HITLAction): boolean => action !== 'rejected' && confirmDisabled;
|
|
116
122
|
|
|
117
123
|
const completionText = $derived(
|
|
@@ -140,6 +146,13 @@
|
|
|
140
146
|
};
|
|
141
147
|
|
|
142
148
|
const restoreMicState = async (): Promise<void> => {
|
|
149
|
+
// An elapsed deadline may decide in the same mount that starts auto-mute.
|
|
150
|
+
// Finish that call and flush its controlled prop before restoring it.
|
|
151
|
+
if (initialMicToggle !== null) {
|
|
152
|
+
await initialMicToggle;
|
|
153
|
+
await tick();
|
|
154
|
+
initialMicToggle = null;
|
|
155
|
+
}
|
|
143
156
|
// Consume restoration before awaiting: a failed decision may be retried
|
|
144
157
|
// before the caller has updated isMicMuted, but must not toggle twice.
|
|
145
158
|
const stateToRestore = originalMicState;
|
|
@@ -214,19 +227,41 @@
|
|
|
214
227
|
return;
|
|
215
228
|
}
|
|
216
229
|
countdownActive = true;
|
|
217
|
-
|
|
218
|
-
|
|
230
|
+
const now = Date.now();
|
|
231
|
+
// Relative duration is anchored once; rerenders must not extend it. An
|
|
232
|
+
// explicit absolute expiry remains live while this timer is running.
|
|
233
|
+
relativeDeadline ??= now + countdownSeconds * 1000;
|
|
234
|
+
const updateCountdown = (): void => {
|
|
219
235
|
// A sibling card's interaction pauses this countdown on its next tick.
|
|
220
|
-
if (
|
|
236
|
+
if (
|
|
237
|
+
countdownSeconds <= 0 ||
|
|
238
|
+
confirmDisabled ||
|
|
239
|
+
isHistoryMode ||
|
|
240
|
+
isCompleted ||
|
|
241
|
+
get(pauseAllConfirmationTimers)
|
|
242
|
+
) {
|
|
221
243
|
stopCountdown();
|
|
222
244
|
return;
|
|
223
245
|
}
|
|
224
|
-
|
|
246
|
+
const currentTime = Date.now();
|
|
247
|
+
const nextDeadline =
|
|
248
|
+
typeof expiresAt === 'number' && Number.isFinite(expiresAt)
|
|
249
|
+
? expiresAt
|
|
250
|
+
: (relativeDeadline ?? now);
|
|
251
|
+
if (nextDeadline !== countdownDeadline) {
|
|
252
|
+
countdownDeadline = nextDeadline;
|
|
253
|
+
countdownDuration = Math.max(0, (nextDeadline - currentTime) / 1000);
|
|
254
|
+
}
|
|
255
|
+
timeRemaining = Math.max(0, (nextDeadline - currentTime) / 1000);
|
|
225
256
|
if (timeRemaining <= 0) {
|
|
226
257
|
stopCountdown();
|
|
227
258
|
void decide('auto-approved');
|
|
228
259
|
}
|
|
229
|
-
}
|
|
260
|
+
};
|
|
261
|
+
updateCountdown();
|
|
262
|
+
if (countdownActive) {
|
|
263
|
+
countdownInterval = setInterval(updateCountdown, 100);
|
|
264
|
+
}
|
|
230
265
|
};
|
|
231
266
|
|
|
232
267
|
// Any interaction pauses this card's own countdown and every sibling's.
|
|
@@ -345,9 +380,15 @@
|
|
|
345
380
|
if (onmictoggle !== null) {
|
|
346
381
|
originalMicState = isMicMuted;
|
|
347
382
|
if (!isMicMuted) {
|
|
348
|
-
|
|
349
|
-
//
|
|
350
|
-
|
|
383
|
+
try {
|
|
384
|
+
// Preserve the existing synchronous mount invocation. Only the
|
|
385
|
+
// decision waits for its result and the controlled prop update.
|
|
386
|
+
initialMicToggle = Promise.resolve(onmictoggle()).catch(() => {
|
|
387
|
+
// Auto-mute is best-effort; settlement still follows a failed call.
|
|
388
|
+
});
|
|
389
|
+
} catch {
|
|
390
|
+
initialMicToggle = Promise.resolve();
|
|
391
|
+
}
|
|
351
392
|
}
|
|
352
393
|
}
|
|
353
394
|
pauseAllConfirmationTimers.set(false);
|
|
@@ -679,7 +720,7 @@
|
|
|
679
720
|
<div class="confirm-button">
|
|
680
721
|
{#if countdownActive && !confirmDisabled}
|
|
681
722
|
<div class="progress-anchor">
|
|
682
|
-
<Progress value={elapsedTime} max={
|
|
723
|
+
<Progress value={elapsedTime} max={countdownDuration} />
|
|
683
724
|
</div>
|
|
684
725
|
{/if}
|
|
685
726
|
<Button
|
|
@@ -144,6 +144,11 @@ export type OptionalHITLProperties = {
|
|
|
144
144
|
* button. `0` disables auto-approval.
|
|
145
145
|
*/
|
|
146
146
|
countdownSeconds?: number;
|
|
147
|
+
/**
|
|
148
|
+
* Absolute server expiry in Unix milliseconds. Updates affect a running
|
|
149
|
+
* countdown; paused or settled cards never restart. Zero keeps manual-only approval.
|
|
150
|
+
*/
|
|
151
|
+
expiresAt?: number;
|
|
147
152
|
/**
|
|
148
153
|
* Seconds until an untouched card auto-rejects — for confirmations that must
|
|
149
154
|
* not auto-approve (e.g. OAuth) but should not block a conversation forever.
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
src,
|
|
18
18
|
type,
|
|
19
19
|
alt = '',
|
|
20
|
+
ariaLabel = 'Video player',
|
|
20
21
|
autoplay = true,
|
|
21
22
|
loop = false,
|
|
22
23
|
controls = false,
|
|
@@ -212,13 +213,6 @@
|
|
|
212
213
|
onpause?.(event);
|
|
213
214
|
}
|
|
214
215
|
|
|
215
|
-
function handleVideoKeydown(event: KeyboardEvent): void {
|
|
216
|
-
if (event.key === 'Enter' || event.key === ' ') {
|
|
217
|
-
event.preventDefault();
|
|
218
|
-
togglePlayback();
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
216
|
function handleTimeUpdate(): void {
|
|
223
217
|
if (videoPlayer === null) {
|
|
224
218
|
return;
|
|
@@ -390,6 +384,7 @@
|
|
|
390
384
|
bind:muted
|
|
391
385
|
{src}
|
|
392
386
|
class="media"
|
|
387
|
+
aria-label={ariaLabel}
|
|
393
388
|
{controls}
|
|
394
389
|
{autoplay}
|
|
395
390
|
{loop}
|
|
@@ -399,11 +394,6 @@
|
|
|
399
394
|
onpause={handlePause}
|
|
400
395
|
ontimeupdate={handleTimeUpdate}
|
|
401
396
|
onloadedmetadata={handleLoadedMetadata}
|
|
402
|
-
onclick={controls ? null : togglePlayback}
|
|
403
|
-
onkeydown={controls ? null : handleVideoKeydown}
|
|
404
|
-
role={controls ? null : 'button'}
|
|
405
|
-
tabindex={controls ? null : 0}
|
|
406
|
-
aria-label={controls ? null : playing ? 'Pause video' : 'Play video'}
|
|
407
397
|
>
|
|
408
398
|
{#if hasCaptions}
|
|
409
399
|
<track
|
|
@@ -417,6 +407,14 @@
|
|
|
417
407
|
</video>
|
|
418
408
|
|
|
419
409
|
{#if !controls}
|
|
410
|
+
<!-- A video cannot carry role="button". The full-surface native button
|
|
411
|
+
preserves pointer/keyboard playback without nesting the other controls. -->
|
|
412
|
+
<button
|
|
413
|
+
type="button"
|
|
414
|
+
class="media-surface-action"
|
|
415
|
+
aria-label={playing ? 'Pause video' : 'Play video'}
|
|
416
|
+
onclick={togglePlayback}
|
|
417
|
+
></button>
|
|
420
418
|
<div class="overlay">
|
|
421
419
|
<div class="center-controls">
|
|
422
420
|
<div class="control center-control">
|
|
@@ -571,6 +569,23 @@
|
|
|
571
569
|
--image-margin: 0px;
|
|
572
570
|
}
|
|
573
571
|
|
|
572
|
+
.media-surface-action {
|
|
573
|
+
position: absolute;
|
|
574
|
+
inset: 0;
|
|
575
|
+
padding: 0;
|
|
576
|
+
border: 0;
|
|
577
|
+
border-radius: inherit;
|
|
578
|
+
background: transparent;
|
|
579
|
+
cursor: pointer;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
.media-surface-action:focus-visible {
|
|
583
|
+
/* Keep both colors inside the clipping container and visible over imagery. */
|
|
584
|
+
outline: var(--media-player-focus-outline, 2px solid #ffffff);
|
|
585
|
+
outline-offset: var(--media-player-focus-outline-offset, -2px);
|
|
586
|
+
box-shadow: var(--media-player-focus-box-shadow, inset 0 0 0 4px #000000);
|
|
587
|
+
}
|
|
588
|
+
|
|
574
589
|
.overlay {
|
|
575
590
|
/* Decorative overlay space leaves the native video pointer route operative. */
|
|
576
591
|
pointer-events: none;
|
|
@@ -601,7 +616,7 @@
|
|
|
601
616
|
reachable for keyboard users too -- :hover alone left them visibility:hidden (and
|
|
602
617
|
so out of the tab order) for anyone not using a mouse. */
|
|
603
618
|
.media-player:hover .overlay,
|
|
604
|
-
.
|
|
619
|
+
.media-player:focus-within .overlay {
|
|
605
620
|
background-color: var(--media-player-overlay-hover-color, #0000004d);
|
|
606
621
|
--center-controls-visibility: visible;
|
|
607
622
|
--bottom-controls-visibility: visible;
|
|
@@ -7,6 +7,8 @@ export type MandatoryMediaPlayerProperties = {
|
|
|
7
7
|
};
|
|
8
8
|
export type OptionalMediaPlayerProperties = {
|
|
9
9
|
alt?: string;
|
|
10
|
+
/** Accessible name for the video. Image naming uses alt. Defaults to "Video player". */
|
|
11
|
+
ariaLabel?: string;
|
|
10
12
|
fallback?: string;
|
|
11
13
|
autoplay?: boolean;
|
|
12
14
|
loop?: boolean;
|