@marianmeres/stuic 3.183.0 → 3.184.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/components/Input/FieldSingleAsset.svelte +76 -18
- package/dist/components/Input/FieldSingleAsset.svelte.d.ts +19 -2
- package/dist/components/Input/README.md +55 -3
- package/dist/components/Input/field-single-asset-i18n-sk.js +1 -0
- package/dist/components/Input/field-single-asset-i18n.d.ts +1 -0
- package/dist/components/Input/field-single-asset-i18n.js +1 -0
- package/dist/components/Input/index.css +16 -3
- package/package.json +1 -1
|
@@ -102,7 +102,8 @@
|
|
|
102
102
|
* The upload. Receives the optimistic asset (its `id` and every `url` are one
|
|
103
103
|
* blob URL of the file) and the file itself; resolves with the stored asset,
|
|
104
104
|
* which becomes the new `value`. A rejection keeps the previous `value`, shows
|
|
105
|
-
* an error state on the tile with Retry / Discard, and reports `notifications
|
|
105
|
+
* an error state on the tile with Retry / Discard, and reports `notifications`
|
|
106
|
+
* (see `onUploadError` to take over that report).
|
|
106
107
|
* Without it the field is display-only (no picker, no drop, no paste).
|
|
107
108
|
*/
|
|
108
109
|
processAsset?: (
|
|
@@ -111,6 +112,17 @@
|
|
|
111
112
|
) => Promise<FieldAsset>;
|
|
112
113
|
/** Render a progress ring driven by `ctx.onProgress` instead of a spinner. */
|
|
113
114
|
withOnProgress?: boolean;
|
|
115
|
+
/**
|
|
116
|
+
* Called when `processAsset` rejects (with the raw rejection, so a status code or
|
|
117
|
+
* a custom error class is still inspectable), after the tile has entered its
|
|
118
|
+
* error state and before the default `notifications.error(...)` toast. Return
|
|
119
|
+
* `false` to skip that toast — for a rejection the page already renders its own
|
|
120
|
+
* way (a quota / plan-limit panel). The tile keeps Retry / Discard regardless.
|
|
121
|
+
*/
|
|
122
|
+
onUploadError?: (
|
|
123
|
+
error: unknown,
|
|
124
|
+
ctx: { asset: FieldAsset; file: File }
|
|
125
|
+
) => void | boolean;
|
|
114
126
|
/** Same tokens as the HTML `accept` attribute; also applied to drops and pastes. */
|
|
115
127
|
accept?: string;
|
|
116
128
|
/** Passed to the file input: on phones opens the camera directly. */
|
|
@@ -133,7 +145,12 @@
|
|
|
133
145
|
transformFile?: (
|
|
134
146
|
file: File
|
|
135
147
|
) => File | null | undefined | Promise<File | null | undefined>;
|
|
136
|
-
/**
|
|
148
|
+
/**
|
|
149
|
+
* Return `false` (may be async) to keep the asset. While a returned promise is
|
|
150
|
+
* pending the tile is busy: the Remove control shows a spinner, the tile and
|
|
151
|
+
* its controls are inert, and a second Remove is a no-op — so the hook may do
|
|
152
|
+
* the actual server-side delete and resolve with whether it succeeded.
|
|
153
|
+
*/
|
|
137
154
|
onBeforeRemove?: (asset: FieldAsset) => boolean | Promise<boolean>;
|
|
138
155
|
/** Return `false` (may be async) to keep the current asset instead of uploading `file`. */
|
|
139
156
|
onBeforeReplace?: (current: FieldAsset, file: File) => boolean | Promise<boolean>;
|
|
@@ -254,6 +271,7 @@
|
|
|
254
271
|
serializeValue = default_serialize,
|
|
255
272
|
processAsset,
|
|
256
273
|
withOnProgress = false,
|
|
274
|
+
onUploadError,
|
|
257
275
|
accept,
|
|
258
276
|
capture,
|
|
259
277
|
maxSize,
|
|
@@ -292,6 +310,9 @@
|
|
|
292
310
|
let pending = $state<Pending | null>(null);
|
|
293
311
|
// Bumped on every new upload / discard so a stale promise cannot land.
|
|
294
312
|
let uploadSeq = 0;
|
|
313
|
+
// `onBeforeRemove` in flight (it may be the real server-side delete): the tile
|
|
314
|
+
// is inert and the Remove control shows a spinner until it settles.
|
|
315
|
+
let removing = $state(false);
|
|
295
316
|
// The last removed asset while its Undo is still offered.
|
|
296
317
|
let removed = $state<FieldAsset | null>(null);
|
|
297
318
|
let undoTimer: ReturnType<typeof setTimeout> | undefined;
|
|
@@ -303,7 +324,9 @@
|
|
|
303
324
|
let descId = $derived(`${id}-action`);
|
|
304
325
|
|
|
305
326
|
let hasLabel = $derived(isTHCNotEmpty(label));
|
|
306
|
-
let canUpload = $derived(
|
|
327
|
+
let canUpload = $derived(
|
|
328
|
+
typeof processAsset === "function" && !disabled && !isLoading && !removing
|
|
329
|
+
);
|
|
307
330
|
// what the tile shows: the upload in progress wins over the committed asset
|
|
308
331
|
let shown = $derived(pending?.asset ?? asset);
|
|
309
332
|
let shownIsImage = $derived(
|
|
@@ -312,13 +335,15 @@
|
|
|
312
335
|
let tileState = $derived(
|
|
313
336
|
isLoading
|
|
314
337
|
? "loading"
|
|
315
|
-
:
|
|
316
|
-
?
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
:
|
|
338
|
+
: removing
|
|
339
|
+
? "removing"
|
|
340
|
+
: pending
|
|
341
|
+
? pending.error
|
|
342
|
+
? "error"
|
|
343
|
+
: "uploading"
|
|
344
|
+
: asset
|
|
345
|
+
? "filled"
|
|
346
|
+
: "empty"
|
|
322
347
|
);
|
|
323
348
|
let sizePreset = $derived(SIZE_PRESETS.includes(size) ? size : undefined);
|
|
324
349
|
let sizeStyle = $derived(
|
|
@@ -337,6 +362,7 @@
|
|
|
337
362
|
);
|
|
338
363
|
let metaText = $derived.by(() => {
|
|
339
364
|
if (!shown) return "";
|
|
365
|
+
if (removing) return t("removing_short");
|
|
340
366
|
if (pending) {
|
|
341
367
|
if (pending.error) return t("upload_failed");
|
|
342
368
|
const parts = [
|
|
@@ -547,7 +573,11 @@
|
|
|
547
573
|
if (pending) pending.error = error;
|
|
548
574
|
const msg = t("upload_failed_named", { name: file.name, error });
|
|
549
575
|
announce(msg);
|
|
550
|
-
|
|
576
|
+
// the consumer may own the report (a quota panel it already renders):
|
|
577
|
+
// `false` skips only the toast, the tile's Retry / Discard stay
|
|
578
|
+
if (onUploadError?.(e, { asset: optimistic, file }) !== false) {
|
|
579
|
+
notifications?.error(msg);
|
|
580
|
+
}
|
|
551
581
|
});
|
|
552
582
|
}
|
|
553
583
|
|
|
@@ -566,8 +596,17 @@
|
|
|
566
596
|
|
|
567
597
|
async function remove() {
|
|
568
598
|
const current = asset;
|
|
569
|
-
if (!current || disabled || isLoading) return;
|
|
570
|
-
if (typeof onBeforeRemove === "function"
|
|
599
|
+
if (!current || disabled || isLoading || removing) return;
|
|
600
|
+
if (typeof onBeforeRemove === "function") {
|
|
601
|
+
// busy for the whole round trip (the hook may be the real delete) — the
|
|
602
|
+
// `removing` guard above is what makes a second click a no-op meanwhile
|
|
603
|
+
removing = true;
|
|
604
|
+
try {
|
|
605
|
+
if (!(await onBeforeRemove(current))) return;
|
|
606
|
+
} finally {
|
|
607
|
+
removing = false;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
571
610
|
assetsPreview?.close?.();
|
|
572
611
|
commit(null);
|
|
573
612
|
announce(t("removed", { name: current.name }));
|
|
@@ -632,20 +671,35 @@
|
|
|
632
671
|
});
|
|
633
672
|
</script>
|
|
634
673
|
|
|
635
|
-
{#snippet control(
|
|
674
|
+
{#snippet control(
|
|
675
|
+
icon: string,
|
|
676
|
+
labelText: string,
|
|
677
|
+
onclick: () => void,
|
|
678
|
+
action: string,
|
|
679
|
+
opts: { busy?: boolean; inert?: boolean } = {}
|
|
680
|
+
)}
|
|
681
|
+
<!-- `aria-disabled` (not `disabled`) so a busy control keeps focus for the
|
|
682
|
+
round trip; the click is a no-op meanwhile (`remove()` guards on `removing`) -->
|
|
636
683
|
<button
|
|
637
684
|
type="button"
|
|
638
685
|
class={twMerge("stuic-field-single-asset-control", classControls)}
|
|
639
686
|
aria-label={labelText}
|
|
687
|
+
aria-disabled={opts.busy || opts.inert ? "true" : undefined}
|
|
688
|
+
aria-busy={opts.busy ? "true" : undefined}
|
|
640
689
|
data-action={action}
|
|
641
690
|
use:tooltip={() => ({ content: labelText })}
|
|
642
691
|
onclick={(e) => {
|
|
643
692
|
e.preventDefault();
|
|
644
693
|
e.stopPropagation();
|
|
694
|
+
if (opts.busy || opts.inert) return;
|
|
645
695
|
onclick();
|
|
646
696
|
}}
|
|
647
697
|
>
|
|
648
|
-
{
|
|
698
|
+
{#if opts.busy}
|
|
699
|
+
<SpinnerCircleOscillate class="size-4" bgStrokeColor="rgba(255 255 255 / 0.3)" />
|
|
700
|
+
{:else}
|
|
701
|
+
{@html icon}
|
|
702
|
+
{/if}
|
|
649
703
|
</button>
|
|
650
704
|
{/snippet}
|
|
651
705
|
|
|
@@ -740,7 +794,8 @@
|
|
|
740
794
|
iconZoomIn({ size: 16 }),
|
|
741
795
|
t("preview"),
|
|
742
796
|
() => assetsPreview.open(0),
|
|
743
|
-
"preview"
|
|
797
|
+
"preview",
|
|
798
|
+
{ inert: removing }
|
|
744
799
|
)}
|
|
745
800
|
{/if}
|
|
746
801
|
{#if pending || !disabled}
|
|
@@ -750,9 +805,12 @@
|
|
|
750
805
|
? pending.error
|
|
751
806
|
? t("discard")
|
|
752
807
|
: t("cancel_upload")
|
|
753
|
-
:
|
|
808
|
+
: removing
|
|
809
|
+
? t("removing_short")
|
|
810
|
+
: t("remove"),
|
|
754
811
|
on_x,
|
|
755
|
-
pending ? "discard" : "remove"
|
|
812
|
+
pending ? "discard" : "remove",
|
|
813
|
+
{ busy: removing }
|
|
756
814
|
)}
|
|
757
815
|
{/if}
|
|
758
816
|
</span>
|
|
@@ -65,12 +65,24 @@ export interface Props extends InputWrapClassProps, Record<string, any> {
|
|
|
65
65
|
* The upload. Receives the optimistic asset (its `id` and every `url` are one
|
|
66
66
|
* blob URL of the file) and the file itself; resolves with the stored asset,
|
|
67
67
|
* which becomes the new `value`. A rejection keeps the previous `value`, shows
|
|
68
|
-
* an error state on the tile with Retry / Discard, and reports `notifications
|
|
68
|
+
* an error state on the tile with Retry / Discard, and reports `notifications`
|
|
69
|
+
* (see `onUploadError` to take over that report).
|
|
69
70
|
* Without it the field is display-only (no picker, no drop, no paste).
|
|
70
71
|
*/
|
|
71
72
|
processAsset?: (asset: FieldAsset, ctx: FieldSingleAssetUploadContext) => Promise<FieldAsset>;
|
|
72
73
|
/** Render a progress ring driven by `ctx.onProgress` instead of a spinner. */
|
|
73
74
|
withOnProgress?: boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Called when `processAsset` rejects (with the raw rejection, so a status code or
|
|
77
|
+
* a custom error class is still inspectable), after the tile has entered its
|
|
78
|
+
* error state and before the default `notifications.error(...)` toast. Return
|
|
79
|
+
* `false` to skip that toast — for a rejection the page already renders its own
|
|
80
|
+
* way (a quota / plan-limit panel). The tile keeps Retry / Discard regardless.
|
|
81
|
+
*/
|
|
82
|
+
onUploadError?: (error: unknown, ctx: {
|
|
83
|
+
asset: FieldAsset;
|
|
84
|
+
file: File;
|
|
85
|
+
}) => void | boolean;
|
|
74
86
|
/** Same tokens as the HTML `accept` attribute; also applied to drops and pastes. */
|
|
75
87
|
accept?: string;
|
|
76
88
|
/** Passed to the file input: on phones opens the camera directly. */
|
|
@@ -89,7 +101,12 @@ export interface Props extends InputWrapClassProps, Record<string, any> {
|
|
|
89
101
|
* cancels silently (the user closed the cropper).
|
|
90
102
|
*/
|
|
91
103
|
transformFile?: (file: File) => File | null | undefined | Promise<File | null | undefined>;
|
|
92
|
-
/**
|
|
104
|
+
/**
|
|
105
|
+
* Return `false` (may be async) to keep the asset. While a returned promise is
|
|
106
|
+
* pending the tile is busy: the Remove control shows a spinner, the tile and
|
|
107
|
+
* its controls are inert, and a second Remove is a no-op — so the hook may do
|
|
108
|
+
* the actual server-side delete and resolve with whether it succeeded.
|
|
109
|
+
*/
|
|
93
110
|
onBeforeRemove?: (asset: FieldAsset) => boolean | Promise<boolean>;
|
|
94
111
|
/** Return `false` (may be async) to keep the current asset instead of uploading `file`. */
|
|
95
112
|
onBeforeReplace?: (current: FieldAsset, file: File) => boolean | Promise<boolean>;
|
|
@@ -768,7 +768,9 @@ built around a single tile instead:
|
|
|
768
768
|
- **The tile is the picker and the drop target.** Click it, drop onto the field, or (opt-in)
|
|
769
769
|
paste — every source _replaces_ what is there. Two files at once are refused.
|
|
770
770
|
- **Remove inline**, with an **Undo** offered for `undoTtl` ms (the asset is only unlinked
|
|
771
|
-
from `value`, never deleted anywhere, so undo is lossless). Focus returns to the tile.
|
|
771
|
+
from `value`, never deleted anywhere, so undo is lossless). Focus returns to the tile. An
|
|
772
|
+
async `onBeforeRemove` gets a busy state for its round trip (spinner on the Remove
|
|
773
|
+
control, tile inert, second click ignored) — so it may be the real server-side delete.
|
|
772
774
|
- **Shape / fit / size presets**: `shape="circle"` for avatars, `shape="wide"` (16:9) for
|
|
773
775
|
banners, `fit="contain"` for logos that must never be cropped; `size` is `sm` / `md` / `lg`
|
|
774
776
|
(5 / 8 / 12rem tall) or any CSS length.
|
|
@@ -776,7 +778,8 @@ built around a single tile instead:
|
|
|
776
778
|
`withOnProgress`.
|
|
777
779
|
- **Rollback on failure.** `value` is only rewritten when the upload _resolves_. A rejection
|
|
778
780
|
keeps the previous asset, shows the error on the tile with **Retry** (same file) and
|
|
779
|
-
**Discard**, and reports through `notifications
|
|
781
|
+
**Discard**, and reports through `notifications` — unless `onUploadError` returns `false`
|
|
782
|
+
because the page shows that failure its own way. X during an upload cancels it (a late
|
|
780
783
|
resolution is ignored).
|
|
781
784
|
- **Client-side checks** before any bytes move: `accept` (also for drops and pastes),
|
|
782
785
|
`maxSize`, a custom async `validateFile`, and a `transformFile` seam for downscaling a
|
|
@@ -852,6 +855,54 @@ or `alert` without one):
|
|
|
852
855
|
one file only → `accept` → `onBeforeReplace` → `transformFile` → `maxSize` → `validateFile`
|
|
853
856
|
→ `processAsset`
|
|
854
857
|
|
|
858
|
+
### When the failure is yours to show (`onUploadError`)
|
|
859
|
+
|
|
860
|
+
Some rejections are not failures but product states — a 402 "plan limit reached" that the
|
|
861
|
+
page already renders as an upgrade panel. Without a hook the field would stack its generic
|
|
862
|
+
toast on top of that panel. `onUploadError(error, { asset, file })` gets the raw rejection
|
|
863
|
+
first; return `false` to skip the toast. The tile still shows the error with Retry / Discard
|
|
864
|
+
(the user may come back to it after upgrading), so the override is deliberately that narrow:
|
|
865
|
+
|
|
866
|
+
```svelte
|
|
867
|
+
<script lang="ts">
|
|
868
|
+
let gated = $state<string | null>(null);
|
|
869
|
+
</script>
|
|
870
|
+
|
|
871
|
+
{#if gated}
|
|
872
|
+
<UpgradeNotice message={gated} />
|
|
873
|
+
{/if}
|
|
874
|
+
|
|
875
|
+
<FieldSingleAsset
|
|
876
|
+
...
|
|
877
|
+
onUploadError={(e) => {
|
|
878
|
+
if ((e as any)?.status === 402) {
|
|
879
|
+
gated = (e as Error).message;
|
|
880
|
+
return false; // handled above — no toast
|
|
881
|
+
}
|
|
882
|
+
}}
|
|
883
|
+
/>
|
|
884
|
+
```
|
|
885
|
+
|
|
886
|
+
### Removing the server copy too (`onBeforeRemove`)
|
|
887
|
+
|
|
888
|
+
By default a remove only unlinks the asset from `value` (hence the lossless Undo). When the
|
|
889
|
+
field owns a file that must go away on the server as well, do the `DELETE` inside
|
|
890
|
+
`onBeforeRemove` and resolve with whether it succeeded — the field clears `value` only then.
|
|
891
|
+
The round trip gets a busy state (spinner on the Remove control, `data-state="removing"`,
|
|
892
|
+
tile and controls inert, a second Remove ignored). Pair it with `undoTtl={0}`: restoring the
|
|
893
|
+
local value would lie about a file that is already gone.
|
|
894
|
+
|
|
895
|
+
```svelte
|
|
896
|
+
<FieldSingleAsset
|
|
897
|
+
...
|
|
898
|
+
undoTtl={0}
|
|
899
|
+
onBeforeRemove={async (asset) => {
|
|
900
|
+
const res = await fetch(`/api/logo/${asset.id}`, { method: "DELETE" });
|
|
901
|
+
return res.ok;
|
|
902
|
+
}}
|
|
903
|
+
/>
|
|
904
|
+
```
|
|
905
|
+
|
|
855
906
|
### Downscale or crop before upload (`transformFile`)
|
|
856
907
|
|
|
857
908
|
```svelte
|
|
@@ -871,12 +922,13 @@ a dialog and resolves with the cropped file.
|
|
|
871
922
|
| `name` | `string` | required | The hidden input's name |
|
|
872
923
|
| `processAsset` | `(asset, { file, onProgress }) => Promise<FieldAsset>` | - | The upload. Without it the field is display-only |
|
|
873
924
|
| `withOnProgress` | `boolean` | `false` | Progress ring instead of a spinner |
|
|
925
|
+
| `onUploadError` | `(error, { asset, file }) => void \| boolean` | - | On a rejected upload, before the toast; `false` skips the toast |
|
|
874
926
|
| `accept` | `string` | - | HTML `accept` tokens (MIME, `image/*`, `.pdf`); applied to drops/pastes |
|
|
875
927
|
| `capture` | `"user" \| "environment"` | - | Passed to the file input: phones open the camera directly |
|
|
876
928
|
| `maxSize` | `number` | - | Max bytes, checked after `transformFile` |
|
|
877
929
|
| `validateFile` | `(file) => string \| void \| Promise<...>` | - | Non-empty string rejects with that message |
|
|
878
930
|
| `transformFile` | `(file) => File \| null \| Promise<...>` | - | Pre-upload hook; `null` cancels |
|
|
879
|
-
| `onBeforeRemove` | `(asset) => boolean \| Promise<boolean>` | - | `false` keeps the asset
|
|
931
|
+
| `onBeforeRemove` | `(asset) => boolean \| Promise<boolean>` | - | `false` keeps the asset; busy state while a promise settles |
|
|
880
932
|
| `onBeforeReplace` | `(current, file) => boolean \| Promise<boolean>` | - | `false` keeps the current asset |
|
|
881
933
|
| `undoTtl` | `number` | `6000` | ms the inline Undo stays after a remove; `0` disables |
|
|
882
934
|
| `pasteable` | `boolean` | `false` | Accept Ctrl/Cmd-V (same routing as `FieldAssets`) |
|
|
@@ -23,6 +23,7 @@ export const FIELD_SINGLE_ASSET_MESSAGES_SK = {
|
|
|
23
23
|
replace_file: "Nahradiť {{name}}",
|
|
24
24
|
empty_hint: "Presuňte sem súbor alebo kliknite a vyberte ho",
|
|
25
25
|
remove: "Odstrániť",
|
|
26
|
+
removing_short: "Odstraňuje sa…",
|
|
26
27
|
removed: "Súbor {{name}} bol odstránený",
|
|
27
28
|
undo: "Vrátiť späť",
|
|
28
29
|
restored: "Súbor {{name}} bol obnovený",
|
|
@@ -13,6 +13,7 @@ export const FIELD_SINGLE_ASSET_MESSAGES_EN = {
|
|
|
13
13
|
replace_file: "Replace {{name}}",
|
|
14
14
|
empty_hint: "Drop a file here or click to browse",
|
|
15
15
|
remove: "Remove",
|
|
16
|
+
removing_short: "Removing…",
|
|
16
17
|
removed: "{{name}} removed",
|
|
17
18
|
undo: "Undo",
|
|
18
19
|
restored: "{{name}} restored",
|
|
@@ -1645,8 +1645,11 @@
|
|
|
1645
1645
|
outline: 2px solid var(--stuic-color-ring);
|
|
1646
1646
|
outline-offset: 2px;
|
|
1647
1647
|
}
|
|
1648
|
-
/* the tile is `disabled` while the field only displays
|
|
1648
|
+
/* the tile is `disabled` while the field only displays (or while an async
|
|
1649
|
+
`onBeforeRemove` is in flight) — keep it legible */
|
|
1649
1650
|
.stuic-field-single-asset[data-state="filled"]
|
|
1651
|
+
.stuic-field-single-asset-preview:disabled,
|
|
1652
|
+
.stuic-field-single-asset[data-state="removing"]
|
|
1650
1653
|
.stuic-field-single-asset-preview:disabled {
|
|
1651
1654
|
opacity: 1;
|
|
1652
1655
|
}
|
|
@@ -1699,9 +1702,12 @@
|
|
|
1699
1702
|
transition: opacity
|
|
1700
1703
|
var(--stuic-field-single-asset-transition, var(--stuic-transition));
|
|
1701
1704
|
}
|
|
1705
|
+
/* pinned open on error (Retry / Discard must not depend on hover) and while a
|
|
1706
|
+
remove is in flight (the busy spinner lives in the Remove control) */
|
|
1702
1707
|
.stuic-field-single-asset-tile:hover .stuic-field-single-asset-actions,
|
|
1703
1708
|
.stuic-field-single-asset-tile:focus-within .stuic-field-single-asset-actions,
|
|
1704
|
-
.stuic-field-single-asset[data-state="error"] .stuic-field-single-asset-actions
|
|
1709
|
+
.stuic-field-single-asset[data-state="error"] .stuic-field-single-asset-actions,
|
|
1710
|
+
.stuic-field-single-asset[data-state="removing"] .stuic-field-single-asset-actions {
|
|
1705
1711
|
opacity: 1;
|
|
1706
1712
|
pointer-events: auto;
|
|
1707
1713
|
}
|
|
@@ -1725,9 +1731,16 @@
|
|
|
1725
1731
|
transition: background-color
|
|
1726
1732
|
var(--stuic-field-single-asset-transition, var(--stuic-transition));
|
|
1727
1733
|
}
|
|
1728
|
-
.stuic-field-single-asset-control:hover {
|
|
1734
|
+
.stuic-field-single-asset-control:hover:not([aria-disabled="true"]) {
|
|
1729
1735
|
background: var(--stuic-field-single-asset-control-bg-hover);
|
|
1730
1736
|
}
|
|
1737
|
+
/* `aria-disabled`, not `disabled`: a busy control keeps focus for the round trip */
|
|
1738
|
+
.stuic-field-single-asset-control[aria-disabled="true"] {
|
|
1739
|
+
cursor: default;
|
|
1740
|
+
}
|
|
1741
|
+
.stuic-field-single-asset-control[aria-disabled="true"]:not([aria-busy="true"]) {
|
|
1742
|
+
opacity: 0.5;
|
|
1743
|
+
}
|
|
1731
1744
|
.stuic-field-single-asset-control:focus-visible {
|
|
1732
1745
|
outline: 2px solid var(--stuic-color-ring);
|
|
1733
1746
|
outline-offset: 1px;
|