@juspay/svelte-ui-components 4.51.0 → 4.52.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/Animations/tokenizedFly.js +9 -2
- package/dist/AreaChart/AreaChart.svelte +41 -13
- package/dist/AreaChart/properties.d.ts +12 -0
- package/dist/AttachmentChipRow/AttachmentChipRow.svelte +7 -6
- package/dist/BarChart/BarChart.svelte +54 -6
- package/dist/BarChart/properties.d.ts +12 -0
- package/dist/Calendar/Calendar.svelte +124 -51
- package/dist/Card/Card.svelte +5 -0
- package/dist/ChatSuggestions/ChatSuggestions.svelte +1 -0
- package/dist/CheckListItem/CheckListItem.svelte +27 -5
- package/dist/ColorPicker/ColorPicker.svelte +40 -9
- package/dist/ColorPicker/properties.d.ts +8 -0
- package/dist/Combobox/Combobox.svelte +34 -2
- package/dist/CommandMenu/CommandMenu.svelte +35 -8
- package/dist/CommandMenu/properties.d.ts +6 -0
- package/dist/DateRangePicker/DateRangePicker.svelte +5 -5
- package/dist/DualAxisBarChart/DualAxisBarChart.svelte +41 -7
- package/dist/DualAxisBarChart/properties.d.ts +4 -0
- package/dist/FileDropzoneTrigger/FileDropzoneTrigger.svelte +104 -10
- package/dist/FileDropzoneTrigger/properties.d.ts +14 -4
- package/dist/FileInput/FileInput.svelte +38 -9
- package/dist/FileInput/context.d.ts +20 -0
- package/dist/FileInput/context.js +8 -0
- package/dist/FileInput/properties.d.ts +25 -1
- package/dist/FunnelChart/FunnelChart.svelte +20 -1
- package/dist/FunnelChart/properties.d.ts +4 -0
- package/dist/Gallery/Gallery.svelte +72 -27
- package/dist/HITL/HITL.svelte +64 -14
- package/dist/HITL/properties.d.ts +5 -0
- package/dist/LineChart/LineChart.svelte +105 -33
- package/dist/LineChart/properties.d.ts +12 -0
- package/dist/MediaPlayer/MediaPlayer.svelte +172 -2
- package/dist/MediaPlayer/MediaPlayer.svelte.d.ts +1 -1
- package/dist/MediaPlayer/properties.d.ts +31 -0
- package/dist/Menu/Menu.svelte +6 -4
- package/dist/Modal/Modal.svelte +15 -12
- package/dist/Modal/properties.d.ts +29 -1
- package/dist/Pagination/Pagination.svelte +1 -0
- package/dist/PieChart/PieChart.svelte +51 -29
- package/dist/PieChart/properties.d.ts +4 -0
- package/dist/Pill/Pill.svelte +5 -0
- package/dist/Radio/Radio.svelte +11 -2
- package/dist/Radio/properties.d.ts +7 -2
- package/dist/SankeyChart/SankeyChart.svelte +339 -211
- package/dist/SankeyChart/properties.d.ts +4 -0
- package/dist/Scroller/Scroller.svelte +236 -4
- package/dist/Scroller/properties.d.ts +8 -0
- package/dist/Select/Select.svelte +46 -8
- package/dist/Select/properties.d.ts +17 -1
- package/dist/Sheet/Sheet.svelte +31 -4
- package/dist/Sheet/properties.d.ts +6 -0
- package/dist/SplitInput/SplitInput.svelte +17 -0
- package/dist/SplitInput/field-name.d.ts +29 -0
- package/dist/SplitInput/field-name.js +50 -0
- package/dist/SplitInput/properties.d.ts +18 -4
- package/dist/Status/Status.svelte +24 -12
- package/dist/Status/properties.d.ts +11 -0
- package/dist/Table/BuiltinCell.svelte +13 -5
- package/dist/Table/BuiltinCell.svelte.d.ts +1 -0
- package/dist/Table/Table.svelte +78 -20
- package/dist/Table/a11y.d.ts +7 -0
- package/dist/Table/a11y.js +71 -0
- package/dist/Table/cellData.js +3 -0
- package/dist/Table/properties.d.ts +13 -5
- package/dist/ThemeSwitcher/ThemeSwitcher.svelte +4 -0
- package/dist/ThinkingIndicator/ThinkingIndicator.svelte +48 -9
- package/dist/Toast/Toast.svelte +70 -4
- package/dist/Toggle/Toggle.svelte +2 -0
- package/dist/Toggle/properties.d.ts +2 -0
- package/dist/_chart/Axis.svelte +10 -0
- package/dist/_chart/ChartContainer.svelte +32 -1
- package/dist/_chart/ChartTooltip.svelte +18 -3
- package/dist/_chart/LabelPlate.svelte +116 -0
- package/dist/_chart/LabelPlate.svelte.d.ts +7 -0
- package/dist/_chart/a11y.d.ts +29 -0
- package/dist/_chart/a11y.js +44 -0
- package/dist/_chart/measure.d.ts +2 -0
- package/dist/_chart/measure.js +4 -0
- package/dist/_chart/tooltipPosition.d.ts +6 -0
- package/dist/_chart/tooltipPosition.js +30 -2
- package/dist/_chart/types.d.ts +22 -0
- package/dist/_interaction/focusable.d.ts +47 -0
- package/dist/_interaction/focusable.js +139 -0
- package/dist/assets/captions-on.svg +3 -0
- package/dist/assets/captions.svg +5 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/styles/theme-dark.css +9 -1
- package/dist/types.d.ts +2 -0
- package/dist/utils.d.ts +18 -2
- package/dist/utils.js +73 -28
- package/dist-codemod/codemod/migrate-audit.js +9 -7
- package/dist-codemod/codemod/migration-assets/legacy-palette.css +9 -1
- package/dist-codemod/codemod/migration-assets-layout.js +61 -0
- package/dist-wc/index.js +5872 -4333
- package/package.json +4 -2
|
@@ -11,14 +11,24 @@ export type OptionalFileDropzoneTriggerProperties = {
|
|
|
11
11
|
/** Bare icon + heading with no Button wrapper or caption, for inline/compact trigger placements. */
|
|
12
12
|
compact?: boolean;
|
|
13
13
|
/**
|
|
14
|
-
* Non-compact: forwarded to the inner `Button
|
|
15
|
-
* Emits both `data-pw` and `testID`
|
|
14
|
+
* Non-compact: forwarded to the inner `Button` when this trigger renders one, otherwise applied to the
|
|
15
|
+
* non-interactive surface. Compact: applied to the heading `<span>`. Emits both `data-pw` and `testID`
|
|
16
|
+
* on that element.
|
|
16
17
|
*/
|
|
17
18
|
testId?: string;
|
|
18
|
-
/** CSS class applied to the root element — the
|
|
19
|
+
/** CSS class applied to the root element — the `Button` (or, inside a `FileInput` region, the non-interactive surface that replaces it) in non-compact, the icon+heading wrapper in compact. */
|
|
19
20
|
classes?: string;
|
|
20
21
|
};
|
|
21
22
|
export type FileDropzoneTriggerEventProperties = {
|
|
22
|
-
/**
|
|
23
|
+
/**
|
|
24
|
+
* Makes the non-compact trigger a real `Button` that owns the action. Use it where nothing else does:
|
|
25
|
+
* standalone, or under `<FileInput activation="trigger">` with `onclick={openFilePicker}`.
|
|
26
|
+
*
|
|
27
|
+
* Inside a default `FileInput` the region already owns activation (one Tab stop; Enter, Space and a
|
|
28
|
+
* click open the chooser), so the trigger renders the same surface as plain content instead of a
|
|
29
|
+
* second button and does not call this handler: the region opens the picker. An existing
|
|
30
|
+
* `onclick={openFilePicker}` there therefore still works, without the duplicate control.
|
|
31
|
+
* Compact never renders a button.
|
|
32
|
+
*/
|
|
23
33
|
onclick?: () => void;
|
|
24
34
|
};
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { describeField } from '../_field/description';
|
|
3
|
+
import { provideFileInputOwner } from './context';
|
|
3
4
|
import type { FileInputProperties } from './properties';
|
|
4
5
|
|
|
5
6
|
let {
|
|
6
7
|
trigger,
|
|
8
|
+
activation = 'region',
|
|
7
9
|
accept,
|
|
8
10
|
multiple = false,
|
|
9
11
|
maxSizeBytes,
|
|
@@ -28,6 +30,18 @@
|
|
|
28
30
|
describeField(fieldUid, { error: errorMessage, info: infoMessage, invalid })
|
|
29
31
|
);
|
|
30
32
|
|
|
33
|
+
/* One upload action, one interactive owner. Left alone, the region is that
|
|
34
|
+
owner and `trigger` content is plain content inside it. A consumer who wants
|
|
35
|
+
a real control inside (a Button with its own loading state, say) hands
|
|
36
|
+
ownership to it instead, and the region stops being a second control: no
|
|
37
|
+
role, no tab stop, no click or key handling -- only drag and drop. */
|
|
38
|
+
const regionOwnsActivation = $derived(activation !== 'trigger');
|
|
39
|
+
provideFileInputOwner({
|
|
40
|
+
get regionOwnsActivation() {
|
|
41
|
+
return regionOwnsActivation;
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
31
45
|
let dragOver = $state(false);
|
|
32
46
|
let inputEl: HTMLInputElement | null = $state(null);
|
|
33
47
|
let pickerBusy = false;
|
|
@@ -149,21 +163,25 @@
|
|
|
149
163
|
}
|
|
150
164
|
</script>
|
|
151
165
|
|
|
166
|
+
<!-- The role is a ternary Svelte cannot resolve, so it reads the element as a plain div. Whenever
|
|
167
|
+
the tabindex renders, the role is "button" on the very same expression. -->
|
|
168
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
152
169
|
<div
|
|
153
170
|
class="file-input {classes ?? ''}"
|
|
154
171
|
class:file-input-dragover={dragOver}
|
|
155
172
|
class:file-input-disabled={disabled}
|
|
173
|
+
class:file-input-passive={!regionOwnsActivation}
|
|
156
174
|
data-pw={testId}
|
|
157
175
|
testID={testId}
|
|
158
|
-
role=
|
|
159
|
-
tabindex={disabled ? -1 : 0}
|
|
160
|
-
aria-disabled={disabled}
|
|
161
|
-
aria-describedby={field.describedBy}
|
|
176
|
+
role={regionOwnsActivation ? 'button' : null}
|
|
177
|
+
tabindex={regionOwnsActivation ? (disabled ? -1 : 0) : null}
|
|
178
|
+
aria-disabled={regionOwnsActivation ? disabled : null}
|
|
179
|
+
aria-describedby={regionOwnsActivation ? field.describedBy : null}
|
|
162
180
|
ondragover={handleDragOver}
|
|
163
181
|
ondragleave={handleDragLeave}
|
|
164
182
|
ondrop={handleDrop}
|
|
165
|
-
onkeydown={handleKeyDown}
|
|
166
|
-
onclick={handleClick}
|
|
183
|
+
onkeydown={regionOwnsActivation ? handleKeyDown : null}
|
|
184
|
+
onclick={regionOwnsActivation ? handleClick : null}
|
|
167
185
|
>
|
|
168
186
|
<input
|
|
169
187
|
bind:this={inputEl}
|
|
@@ -179,7 +197,7 @@
|
|
|
179
197
|
aria-hidden="true"
|
|
180
198
|
/>
|
|
181
199
|
|
|
182
|
-
{@render trigger({ openFilePicker, dragOver, disabled })}
|
|
200
|
+
{@render trigger({ openFilePicker, dragOver, disabled, describedBy: field.describedBy })}
|
|
183
201
|
</div>
|
|
184
202
|
|
|
185
203
|
{#if field.showsError}
|
|
@@ -231,9 +249,20 @@
|
|
|
231
249
|
cursor: pointer;
|
|
232
250
|
}
|
|
233
251
|
|
|
252
|
+
/* The region is not the control here, so it must not look like one. */
|
|
253
|
+
.file-input-passive {
|
|
254
|
+
cursor: auto;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/* The region is the one Tab stop of its upload action, so it is also the only thing that can
|
|
258
|
+
show where focus is. With no fallback this resolved to `outline: none` -- invisible to a
|
|
259
|
+
keyboard user, and only masked while a nested button supplied its own ring. `currentColor`
|
|
260
|
+
is the library's convention for a ring that needs no colour literal (Card, Pill, StatCard):
|
|
261
|
+
it follows the surrounding text colour, so it clears 3:1 on the light and the dark ground
|
|
262
|
+
alike and stays visible in forced-colors mode. :focus-visible keeps it off pointer clicks. */
|
|
234
263
|
.file-input:focus-visible {
|
|
235
|
-
outline: var(--file-input-focus-outline);
|
|
236
|
-
outline-offset: var(--file-input-focus-outline-offset);
|
|
264
|
+
outline: var(--file-input-focus-outline, 2px solid currentColor);
|
|
265
|
+
outline-offset: var(--file-input-focus-outline-offset, 2px);
|
|
237
266
|
}
|
|
238
267
|
|
|
239
268
|
.file-input-dragover {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a `FileInput` tells the content rendered inside its `trigger` snippet.
|
|
3
|
+
*
|
|
4
|
+
* A drop region is one control. By default the region itself is that control
|
|
5
|
+
* (`role="button"`, one Tab stop, Enter/Space/click open the chooser), so
|
|
6
|
+
* anything interactive placed inside it is a second control for the same
|
|
7
|
+
* action. `FileDropzoneTrigger` reads this to render as plain content when the
|
|
8
|
+
* region owns activation, instead of nesting a native `<button>` in it.
|
|
9
|
+
*
|
|
10
|
+
* A getter, not a copied boolean, so a consumer flipping `activation` on a live
|
|
11
|
+
* region is seen by content that is already mounted.
|
|
12
|
+
*/
|
|
13
|
+
export type FileInputOwner = {
|
|
14
|
+
readonly regionOwnsActivation: boolean;
|
|
15
|
+
};
|
|
16
|
+
/** Exported only so a test can stand in for the region without mounting a full `FileInput`. */
|
|
17
|
+
export declare const FILE_INPUT_OWNER: unique symbol;
|
|
18
|
+
export declare const provideFileInputOwner: (owner: FileInputOwner) => void;
|
|
19
|
+
/** Null outside a `FileInput`, where nothing owns the action but the caller. */
|
|
20
|
+
export declare const useFileInputOwner: () => FileInputOwner | null;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { getContext, hasContext, setContext } from 'svelte';
|
|
2
|
+
/** Exported only so a test can stand in for the region without mounting a full `FileInput`. */
|
|
3
|
+
export const FILE_INPUT_OWNER = Symbol('sui-file-input-owner');
|
|
4
|
+
export const provideFileInputOwner = (owner) => {
|
|
5
|
+
setContext(FILE_INPUT_OWNER, owner);
|
|
6
|
+
};
|
|
7
|
+
/** Null outside a `FileInput`, where nothing owns the action but the caller. */
|
|
8
|
+
export const useFileInputOwner = () => hasContext(FILE_INPUT_OWNER) ? getContext(FILE_INPUT_OWNER) : null;
|
|
@@ -3,10 +3,34 @@ export type FileInputSnippetProps = {
|
|
|
3
3
|
openFilePicker: () => void;
|
|
4
4
|
dragOver: boolean;
|
|
5
5
|
disabled: boolean;
|
|
6
|
+
/**
|
|
7
|
+
* The ids of the rendered error/info messages, for `aria-describedby`. The drop region links
|
|
8
|
+
* them itself while it owns activation; a control supplied under `activation="trigger"` is the
|
|
9
|
+
* one that must carry them, so it is handed here. Null when nothing is rendered.
|
|
10
|
+
*/
|
|
11
|
+
describedBy: string | null;
|
|
6
12
|
};
|
|
13
|
+
/**
|
|
14
|
+
* Which element is the single interactive control for the upload action.
|
|
15
|
+
*
|
|
16
|
+
* - `'region'` (default): the drop region is the control. It is the one Tab stop, announces as
|
|
17
|
+
* a button named from the `trigger` content, and opens the chooser on Enter, Space or a click
|
|
18
|
+
* anywhere inside it. `trigger` content must therefore be non-interactive.
|
|
19
|
+
* - `'trigger'`: the control inside `trigger` (a `Button`, say) is the one Tab stop and calls
|
|
20
|
+
* `openFilePicker`. The region becomes a passive drag-and-drop target with no role, no tab
|
|
21
|
+
* stop and no click or key handling of its own.
|
|
22
|
+
*/
|
|
23
|
+
export type FileInputActivation = 'region' | 'trigger';
|
|
7
24
|
export type FileInputProperties = {
|
|
8
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* Content slot — receives { openFilePicker, dragOver, disabled, describedBy } so consumers build
|
|
27
|
+
* any visual they need. Under the default `activation="region"` it must be non-interactive: the
|
|
28
|
+
* region is already the control, and a button inside it is a second Tab stop and a second
|
|
29
|
+
* announcement for the same action.
|
|
30
|
+
*/
|
|
9
31
|
trigger: Snippet<[FileInputSnippetProps]>;
|
|
32
|
+
/** Who owns the single interactive control. Defaults to `'region'`; see {@link FileInputActivation}. */
|
|
33
|
+
activation?: FileInputActivation;
|
|
10
34
|
accept?: string;
|
|
11
35
|
multiple?: boolean;
|
|
12
36
|
maxSizeBytes?: number;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { onMount } from 'svelte';
|
|
3
3
|
import type { FunnelChartProperties, FunnelStage } from './properties';
|
|
4
4
|
import ChartContainer from '../_chart/ChartContainer.svelte';
|
|
5
|
+
import { defaultChartName, resolveChartName } from '../_chart/a11y';
|
|
5
6
|
import ChartTooltip from '../_chart/ChartTooltip.svelte';
|
|
6
7
|
import { getColor, getContrastColor } from '../_chart/colors';
|
|
7
8
|
import { formatNumber, formatPercent } from '../_chart/format';
|
|
@@ -25,6 +26,8 @@
|
|
|
25
26
|
minHeight = 0,
|
|
26
27
|
radius = DEFAULT_CHART_CORNER_RADIUS,
|
|
27
28
|
tooltipPortal = false,
|
|
29
|
+
ariaLabel,
|
|
30
|
+
ariaDescription,
|
|
28
31
|
testId,
|
|
29
32
|
classes,
|
|
30
33
|
empty,
|
|
@@ -92,6 +95,16 @@
|
|
|
92
95
|
// The documented contract treats an all-zero dataset as empty (it would otherwise
|
|
93
96
|
// render meaningless min-height bars), so fold that into the empty check.
|
|
94
97
|
let isEmpty = $derived(data.length === 0 || data.every((stage) => stage.value === 0));
|
|
98
|
+
const chartName = $derived(
|
|
99
|
+
resolveChartName(
|
|
100
|
+
ariaLabel,
|
|
101
|
+
defaultChartName('funnel', {
|
|
102
|
+
seriesNames: data.map((stage) => stage.category)
|
|
103
|
+
})
|
|
104
|
+
)
|
|
105
|
+
);
|
|
106
|
+
// Zero-valued stages still paint structural bars and retain their callbacks.
|
|
107
|
+
const hasMarks = $derived(data.length > 0);
|
|
95
108
|
|
|
96
109
|
/**
|
|
97
110
|
* Computes the total horizontal space consumed by slope connectors.
|
|
@@ -330,6 +343,9 @@
|
|
|
330
343
|
{aspectRatio}
|
|
331
344
|
{maxHeight}
|
|
332
345
|
{minHeight}
|
|
346
|
+
ariaLabel={chartName}
|
|
347
|
+
{ariaDescription}
|
|
348
|
+
interactive={hasMarks}
|
|
333
349
|
>
|
|
334
350
|
<g transform="translate({MARGIN_LEFT}, {MARGIN_TOP})">
|
|
335
351
|
<!-- Stage bars and category labels -->
|
|
@@ -344,6 +360,7 @@
|
|
|
344
360
|
<!-- Category label above the bar -->
|
|
345
361
|
<text
|
|
346
362
|
class="funnel-category-label"
|
|
363
|
+
aria-hidden="true"
|
|
347
364
|
x={labelX}
|
|
348
365
|
y={labelAreaHeight - 6}
|
|
349
366
|
text-anchor="middle"
|
|
@@ -371,8 +388,9 @@
|
|
|
371
388
|
onblur={handleLeave}
|
|
372
389
|
onkeydown={(e) => handleKeydown(e, index)}
|
|
373
390
|
onclick={() => handleClick(index)}
|
|
374
|
-
tabindex=
|
|
391
|
+
tabindex={hasMarks ? 0 : null}
|
|
375
392
|
role="button"
|
|
393
|
+
aria-hidden={hasMarks ? null : 'true'}
|
|
376
394
|
/>
|
|
377
395
|
|
|
378
396
|
<!-- Value label centred inside the bar -->
|
|
@@ -381,6 +399,7 @@
|
|
|
381
399
|
{#if vl !== ''}
|
|
382
400
|
<text
|
|
383
401
|
class="funnel-value-label"
|
|
402
|
+
aria-hidden="true"
|
|
384
403
|
x={labelX}
|
|
385
404
|
y={by + bh / 2}
|
|
386
405
|
text-anchor="middle"
|
|
@@ -72,6 +72,10 @@ export type OptionalFunnelChartProperties = {
|
|
|
72
72
|
*/
|
|
73
73
|
tooltipPortal?: boolean;
|
|
74
74
|
/** Value for the `data-pw` attribute on the chart root element. */
|
|
75
|
+
/** Accessible name of the drawing; omit to derive one from the plotted data. */
|
|
76
|
+
ariaLabel?: string;
|
|
77
|
+
/** Longer chart description, resolved inside the drawing's own DOM/shadow root. */
|
|
78
|
+
ariaDescription?: string;
|
|
75
79
|
testId?: string;
|
|
76
80
|
/** CSS class string applied to the chart root element. Useful for CSS-variable theming. */
|
|
77
81
|
classes?: string;
|
|
@@ -283,27 +283,29 @@
|
|
|
283
283
|
</div>
|
|
284
284
|
{/if}
|
|
285
285
|
{#if showItemActions}
|
|
286
|
-
<div class="gallery-item-actions">
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
<
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
<
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
286
|
+
<div class="gallery-item-actions-frame">
|
|
287
|
+
<div class="gallery-item-actions">
|
|
288
|
+
{#if showEditButton}
|
|
289
|
+
<div class="gallery-item-action">
|
|
290
|
+
<Button
|
|
291
|
+
ariaLabel={`Edit image ${index + 1}: ${image.alt}`}
|
|
292
|
+
onclick={(event) => handleEditClick(index, event)}
|
|
293
|
+
>
|
|
294
|
+
{@render controlIcon(editSvg, editIcon)}
|
|
295
|
+
</Button>
|
|
296
|
+
</div>
|
|
297
|
+
{/if}
|
|
298
|
+
{#if showDeleteButton}
|
|
299
|
+
<div class="gallery-item-action">
|
|
300
|
+
<Button
|
|
301
|
+
ariaLabel={`Delete image ${index + 1}: ${image.alt}`}
|
|
302
|
+
onclick={(event) => handleDeleteClick(index, event)}
|
|
303
|
+
>
|
|
304
|
+
{@render controlIcon(deleteSvg, deleteIcon)}
|
|
305
|
+
</Button>
|
|
306
|
+
</div>
|
|
307
|
+
{/if}
|
|
308
|
+
</div>
|
|
307
309
|
</div>
|
|
308
310
|
{/if}
|
|
309
311
|
</div>
|
|
@@ -514,16 +516,43 @@
|
|
|
514
516
|
font-family: var(--gallery-list-caption-font-family, inherit);
|
|
515
517
|
}
|
|
516
518
|
|
|
519
|
+
/* The --_actions-* / --_action-* values are internal fallbacks that only the narrow-tile rule at the
|
|
520
|
+
end of this block changes. They sit BELOW the public --gallery-item-action* variables, so any value
|
|
521
|
+
a consumer sets still wins at every tile width. */
|
|
517
522
|
.gallery-item-actions {
|
|
518
523
|
display: flex;
|
|
519
524
|
align-items: center;
|
|
520
|
-
gap: var(--gallery-item-actions-gap, 4px);
|
|
525
|
+
gap: var(--gallery-item-actions-gap, var(--_actions-gap, 4px));
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/* The frame exists so the narrow-tile rule below has a container to ask. It cannot be the tile itself:
|
|
529
|
+
inline-size containment makes a box report no intrinsic width, and a tile that reports none leaves a
|
|
530
|
+
grid inside an ancestor that sizes to its content (inline-block, fit-content, an auto-width absolute
|
|
531
|
+
box) with nothing to measure, so the whole gallery collapsed to its gaps. An out-of-flow box adds
|
|
532
|
+
nothing to its parent's intrinsic size, so the frame can be contained and the tile sizes as before.
|
|
533
|
+
It covers the tile exactly and lets pointer events through to the image underneath; in list view it
|
|
534
|
+
takes no box at all. Named so the rule can only match these frames, never a container in the
|
|
535
|
+
consumer's own page. */
|
|
536
|
+
.gallery-item-actions-frame {
|
|
537
|
+
display: contents;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
.grid .gallery-item-actions-frame {
|
|
541
|
+
display: block;
|
|
542
|
+
position: absolute;
|
|
543
|
+
top: 0;
|
|
544
|
+
right: 0;
|
|
545
|
+
bottom: 0;
|
|
546
|
+
left: 0;
|
|
547
|
+
pointer-events: none;
|
|
548
|
+
container: gallery-tile / inline-size;
|
|
521
549
|
}
|
|
522
550
|
|
|
523
551
|
.grid .gallery-item-actions {
|
|
524
552
|
position: absolute;
|
|
525
|
-
|
|
526
|
-
|
|
553
|
+
pointer-events: auto;
|
|
554
|
+
top: var(--gallery-item-actions-top, var(--_actions-inset, 8px));
|
|
555
|
+
right: var(--gallery-item-actions-right, var(--_actions-inset, 8px));
|
|
527
556
|
}
|
|
528
557
|
|
|
529
558
|
.list .gallery-item-actions {
|
|
@@ -535,12 +564,28 @@
|
|
|
535
564
|
border-radius: var(--gallery-item-action-border-radius, 8px);
|
|
536
565
|
--button-color: transparent;
|
|
537
566
|
--button-hover-color: transparent;
|
|
538
|
-
--button-padding: var(--gallery-item-action-padding, 6px);
|
|
567
|
+
--button-padding: var(--gallery-item-action-padding, var(--_action-padding, 6px));
|
|
539
568
|
--button-border-radius: var(--gallery-item-action-border-radius, 8px);
|
|
540
569
|
--icon-container-padding: 0px;
|
|
541
570
|
--icon-padding: 0px;
|
|
542
|
-
--icon-width: var(--gallery-item-action-icon-size, 16px);
|
|
543
|
-
--icon-height: var(--gallery-item-action-icon-size, 16px);
|
|
571
|
+
--icon-width: var(--gallery-item-action-icon-size, var(--_action-icon, 16px));
|
|
572
|
+
--icon-height: var(--gallery-item-action-icon-size, var(--_action-icon, 16px));
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/* Two default-size actions need 2 x 36px + a 4px gap + both 8px insets = 92px of tile width. A tile
|
|
576
|
+
narrower than that (a three-column grid on a 320px phone is about 79px) pushed the left button past
|
|
577
|
+
the tile's overflow:hidden edge. Below 96px the defaults shrink instead: 26px buttons (still above the
|
|
578
|
+
24px WCAG 2.5.8 target minimum), 2px gap, 4px insets. */
|
|
579
|
+
@container gallery-tile (max-width: 96px) {
|
|
580
|
+
.gallery-item-actions {
|
|
581
|
+
--_actions-gap: 2px;
|
|
582
|
+
--_actions-inset: 4px;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
.gallery-item-action {
|
|
586
|
+
--_action-padding: 3px;
|
|
587
|
+
--_action-icon: 12px;
|
|
588
|
+
}
|
|
544
589
|
}
|
|
545
590
|
|
|
546
591
|
.grid .gallery-item-action {
|
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
|
|
@@ -833,16 +874,25 @@
|
|
|
833
874
|
--input-container-width: 100%;
|
|
834
875
|
}
|
|
835
876
|
|
|
877
|
+
/* Wraps so a narrow card (a phone, a chat column) puts the buttons that do not
|
|
878
|
+
fit on their own line instead of running past the card. Each wrapper is
|
|
879
|
+
`flex: 1`, and its automatic minimum is its button's text width, so a row
|
|
880
|
+
that fits is laid out exactly as before; one that does not breaks between
|
|
881
|
+
buttons and the ones on a short line grow to fill it. `.hitl` is
|
|
882
|
+
`contain: layout`, so an overflowing button here would be invisible to the
|
|
883
|
+
page's own scrollWidth and unreachable by scrolling. The wrapping itself comes from the
|
|
884
|
+
zero-specificity rule below, so --hitl-buttons-wrap: nowrap still turns it off. */
|
|
836
885
|
.action-buttons {
|
|
837
886
|
display: flex;
|
|
838
887
|
gap: var(--hitl-buttons-gap, 0.75rem);
|
|
839
888
|
width: 100%;
|
|
840
889
|
}
|
|
841
890
|
|
|
842
|
-
/* Zero specificity, so a consumer's own flex-wrap rule for the row keeps winning
|
|
843
|
-
|
|
891
|
+
/* Zero specificity, so a consumer's own flex-wrap rule for the row keeps winning. The default
|
|
892
|
+
wraps, because a row that does not fit otherwise runs past the card; a row that fits is laid
|
|
893
|
+
out exactly as before. Set --hitl-buttons-wrap: nowrap to keep the single line. */
|
|
844
894
|
:where(.action-buttons) {
|
|
845
|
-
flex-wrap: var(--hitl-buttons-wrap,
|
|
895
|
+
flex-wrap: var(--hitl-buttons-wrap, wrap);
|
|
846
896
|
}
|
|
847
897
|
|
|
848
898
|
.cancel-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.
|