@juspay/svelte-ui-components 4.33.3 → 4.34.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/Accordion/Accordion.svelte +26 -2
- package/dist/Accordion/properties.d.ts +7 -0
- package/dist/ArmedButton/ArmedButton.svelte +147 -0
- package/dist/ArmedButton/ArmedButton.svelte.d.ts +4 -0
- package/dist/ArmedButton/properties.d.ts +24 -0
- package/dist/ArmedButton/properties.js +1 -0
- package/dist/Banner/Banner.svelte +13 -8
- package/dist/Banner/properties.d.ts +8 -1
- package/dist/Button/Button.svelte +26 -0
- package/dist/Button/properties.d.ts +17 -0
- package/dist/ChatComposer/ChatComposer.svelte +314 -1
- package/dist/ChatComposer/properties.d.ts +42 -0
- package/dist/ChatMessageList/ChatMessageList.svelte +22 -2
- package/dist/ChatMessageList/ChatMessageList.svelte.d.ts +1 -1
- package/dist/CommandMenu/CommandMenu.svelte +53 -20
- package/dist/CommandMenu/properties.d.ts +24 -0
- package/dist/DiffViewer/DiffViewer.svelte +210 -0
- package/dist/DiffViewer/DiffViewer.svelte.d.ts +4 -0
- package/dist/DiffViewer/properties.d.ts +25 -0
- package/dist/DiffViewer/properties.js +1 -0
- package/dist/EmptyState/EmptyState.svelte +22 -3
- package/dist/EmptyState/properties.d.ts +9 -0
- package/dist/HITL/HITL.svelte +320 -51
- package/dist/HITL/properties.d.ts +113 -11
- package/dist/Input/Input.svelte +45 -1
- package/dist/Input/properties.d.ts +15 -0
- package/dist/KeyValue/KeyValue.svelte +16 -3
- package/dist/KeyValue/properties.d.ts +2 -0
- package/dist/Progress/Progress.svelte +96 -41
- package/dist/Progress/properties.d.ts +19 -0
- package/dist/Snippet/Snippet.svelte +45 -1
- package/dist/Snippet/properties.d.ts +8 -0
- package/dist/Tabs/Tabs.svelte +75 -16
- package/dist/Tabs/properties.d.ts +21 -0
- package/dist/ThinkingIndicator/ThinkingIndicator.svelte.d.ts +1 -1
- package/dist/Toolbar/Toolbar.svelte +81 -3
- package/dist/Toolbar/properties.d.ts +37 -0
- package/dist/WindowedList/WindowedList.svelte +129 -0
- package/dist/WindowedList/WindowedList.svelte.d.ts +25 -0
- package/dist/WindowedList/properties.d.ts +38 -0
- package/dist/WindowedList/properties.js +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +3 -0
- package/dist/styles/theme-dark.css +42 -0
- package/dist-wc/index.js +2637 -1642
- package/package.json +2 -1
package/dist/Input/Input.svelte
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
placeholder = '',
|
|
12
12
|
dataType = 'text',
|
|
13
13
|
label = '',
|
|
14
|
+
labelSuffix,
|
|
15
|
+
autofocus = false,
|
|
14
16
|
onErrorMessage = '',
|
|
15
17
|
infoMessage = '',
|
|
16
18
|
validators = [],
|
|
@@ -110,6 +112,19 @@
|
|
|
110
112
|
|
|
111
113
|
let inputElement: HTMLInputElement | HTMLTextAreaElement | null = $state(null);
|
|
112
114
|
|
|
115
|
+
// Guarded to once per mount: without hasAutoFocused, a later element swap
|
|
116
|
+
// (useTextArea toggling, or a left/right-icon layout change moving
|
|
117
|
+
// fieldElement's render site) replaces inputElement and this effect would
|
|
118
|
+
// refocus the field again, long after the user may have moved on.
|
|
119
|
+
let hasAutoFocused = false;
|
|
120
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
121
|
+
$effect(() => {
|
|
122
|
+
if (autofocus && inputElement && !hasAutoFocused) {
|
|
123
|
+
inputElement.focus();
|
|
124
|
+
hasAutoFocused = true;
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
113
128
|
let validationState = $derived.by(() => {
|
|
114
129
|
const valueValidation: ValidationState = validateInput(
|
|
115
130
|
value,
|
|
@@ -313,7 +328,16 @@
|
|
|
313
328
|
</script>
|
|
314
329
|
|
|
315
330
|
<div class="input-container {classes ?? ''}" class:input-error={showError && !actionInput}>
|
|
316
|
-
|
|
331
|
+
<!-- One chain, so the label-only default is the same single block it always was. -->
|
|
332
|
+
{#if hasVisibleLabel && typeof labelSuffix === 'function'}
|
|
333
|
+
<div class="label-row">
|
|
334
|
+
<label class="label" for={effectiveId}>
|
|
335
|
+
{label}{#if isRequired}<span class="input-mandatory-asterisk" aria-hidden="true">*</span
|
|
336
|
+
>{/if}
|
|
337
|
+
</label>
|
|
338
|
+
<span>{@render labelSuffix()}</span>
|
|
339
|
+
</div>
|
|
340
|
+
{:else if hasVisibleLabel}
|
|
317
341
|
<label class="label" for={effectiveId}>
|
|
318
342
|
{label}{#if isRequired}<span class="input-mandatory-asterisk" aria-hidden="true">*</span>{/if}
|
|
319
343
|
</label>
|
|
@@ -542,6 +566,26 @@
|
|
|
542
566
|
width: var(--input-container-width, fit-content);
|
|
543
567
|
}
|
|
544
568
|
|
|
569
|
+
/* Only present when labelSuffix is supplied (and written as a direct child of the
|
|
570
|
+
container, so a `label-row` class a consumer passes through `classes`, which lands
|
|
571
|
+
on the container itself, cannot match): a flex item's own margin is
|
|
572
|
+
space around just that item, not something later content in the column
|
|
573
|
+
can rely on to open up room below the whole row, so the row itself
|
|
574
|
+
carries the margin a lone .label used to carry on its own below. */
|
|
575
|
+
.input-container > .label-row {
|
|
576
|
+
display: flex;
|
|
577
|
+
align-items: baseline;
|
|
578
|
+
justify-content: space-between;
|
|
579
|
+
gap: var(--input-label-row-gap, 8px);
|
|
580
|
+
margin: var(--input-label-msg-margin, 0px 0px 6px 0px);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/* A row-wrapped label hands its margin to the row above, so it does not
|
|
584
|
+
take it twice. Only exists in the labelSuffix branch. */
|
|
585
|
+
.input-container > .label-row > .label {
|
|
586
|
+
margin: 0;
|
|
587
|
+
}
|
|
588
|
+
|
|
545
589
|
.label {
|
|
546
590
|
font-weight: var(--input-label-msg-text-weight, 400);
|
|
547
591
|
font-size: var(--input-label-msg-text-size, 12px);
|
|
@@ -32,6 +32,21 @@ export type OptionalInputProperties = {
|
|
|
32
32
|
*/
|
|
33
33
|
dataType?: InputDataType;
|
|
34
34
|
label?: string | null;
|
|
35
|
+
/**
|
|
36
|
+
* Rendered beside the label, right-aligned (a status Pill, a character
|
|
37
|
+
* count, a short hint). It sits outside the `<label>`, so it never joins the
|
|
38
|
+
* field's accessible name, but it stays readable to assistive tech. Mark a
|
|
39
|
+
* purely decorative suffix `aria-hidden` yourself. Ignored without a
|
|
40
|
+
* visible `label` (there is no label row for it to sit in).
|
|
41
|
+
*/
|
|
42
|
+
labelSuffix?: Snippet;
|
|
43
|
+
/**
|
|
44
|
+
* Focuses the field once it exists, via a plain native `.focus()` —
|
|
45
|
+
* deliberately not the exported `focus()` method, which also
|
|
46
|
+
* `scrollIntoView`s smoothly; a field that already exists to be typed
|
|
47
|
+
* into (a single-field dialog, say) does not need to scroll to itself.
|
|
48
|
+
*/
|
|
49
|
+
autofocus?: boolean;
|
|
35
50
|
onErrorMessage?: string | null;
|
|
36
51
|
infoMessage?: string | null;
|
|
37
52
|
validators?: CustomValidator[];
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
{item.label}
|
|
41
41
|
{/if}
|
|
42
42
|
</dt>
|
|
43
|
-
<dd class="key-value-value">
|
|
43
|
+
<dd class="key-value-value" title={item.title}>
|
|
44
44
|
{#if valueSnippet}
|
|
45
45
|
{@render valueSnippet(item, index)}
|
|
46
46
|
{:else}
|
|
@@ -104,8 +104,12 @@
|
|
|
104
104
|
line-height: var(--keyvalue-label-line-height, 1.4);
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
/* The two cells' flex as variables, not just the label's basis: a label
|
|
108
|
+
that grows and truncates beside a content-sized value (a long name with a
|
|
109
|
+
short figure on the right) needs grow/shrink too, and a consumer's own
|
|
110
|
+
.key-value-label rule could only lose to this scoped one. */
|
|
107
111
|
.key-value--horizontal .key-value-label {
|
|
108
|
-
flex: 0 0 var(--keyvalue-label-width, 140px);
|
|
112
|
+
flex: var(--keyvalue-label-flex, 0 0 var(--keyvalue-label-width, 140px));
|
|
109
113
|
}
|
|
110
114
|
|
|
111
115
|
.key-value-value {
|
|
@@ -117,8 +121,17 @@
|
|
|
117
121
|
overflow-wrap: anywhere;
|
|
118
122
|
}
|
|
119
123
|
|
|
124
|
+
/* Unset, each inherits -- exactly what the cell did before these hooks. Zero specificity,
|
|
125
|
+
so a consumer's own rule on the cell (`dd { text-align: right }`) keeps winning, as it
|
|
126
|
+
did before the hooks existed. */
|
|
127
|
+
:where(.key-value-value) {
|
|
128
|
+
font-family: var(--keyvalue-value-font-family, inherit);
|
|
129
|
+
text-align: var(--keyvalue-value-text-align, inherit);
|
|
130
|
+
font-variant-ligatures: var(--keyvalue-value-font-variant-ligatures, inherit);
|
|
131
|
+
}
|
|
132
|
+
|
|
120
133
|
.key-value--horizontal .key-value-value {
|
|
121
|
-
flex: 1 1 auto;
|
|
134
|
+
flex: var(--keyvalue-value-flex, 1 1 auto);
|
|
122
135
|
min-width: 0;
|
|
123
136
|
}
|
|
124
137
|
|
|
@@ -8,6 +8,8 @@ export type KeyValueItem = {
|
|
|
8
8
|
* `hideEmpty` is `true` (the default), or rendered as `emptyText` otherwise.
|
|
9
9
|
*/
|
|
10
10
|
value?: string | number | null;
|
|
11
|
+
/** Tooltip for the value cell, as its native `title` (e.g. the full path behind a shortened one). */
|
|
12
|
+
title?: string;
|
|
11
13
|
/** Optional per-item test id, emitted as `data-pw` on the item wrapper. */
|
|
12
14
|
testId?: string;
|
|
13
15
|
};
|
|
@@ -5,9 +5,13 @@
|
|
|
5
5
|
let {
|
|
6
6
|
value,
|
|
7
7
|
max = 100,
|
|
8
|
+
leadingLabel,
|
|
8
9
|
showLabel = false,
|
|
9
10
|
animateValue = false,
|
|
10
11
|
ariaLabel,
|
|
12
|
+
headerStart,
|
|
13
|
+
headerEnd,
|
|
14
|
+
note,
|
|
11
15
|
testId,
|
|
12
16
|
classes
|
|
13
17
|
}: ProgressProperties = $props();
|
|
@@ -40,49 +44,89 @@
|
|
|
40
44
|
let labelText = $derived(isIndeterminate ? 'Loading' : `${Math.round(percentage)}%`);
|
|
41
45
|
</script>
|
|
42
46
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
`format`: `AnimatedNumber`'s Intl percent style expects a 0-1
|
|
68
|
-
fraction and would re-derive a different number from this
|
|
69
|
-
already-0-100 one. The % is a literal beside it instead, exactly
|
|
70
|
-
mirroring what labelText already spells out below. -->
|
|
71
|
-
<!-- The unit goes INSIDE AnimatedNumber, not beside it. The odometer
|
|
72
|
-
carries role="img" with its own aria-label, so a unit left outside is
|
|
73
|
-
invisible to anyone navigating by graphic -- the rotor reads "75" with
|
|
74
|
-
no sign of what it measures. Passing the formatted string keeps the
|
|
75
|
-
unit in the accessible name; only the digits move, since the string
|
|
76
|
-
path rolls the characters that changed and leaves the rest. -->
|
|
77
|
-
<AnimatedNumber value={`${Math.round(percentage)}%`} />
|
|
78
|
-
{:else}
|
|
79
|
-
{labelText}
|
|
80
|
-
{/if}
|
|
47
|
+
<!-- The bar itself. Rendered bare, exactly as it always was, unless a header
|
|
48
|
+
or note is passed: only then does a `.progress-root` wrap it. -->
|
|
49
|
+
{#snippet trackRow()}
|
|
50
|
+
<div
|
|
51
|
+
class="container {classes ?? ''}"
|
|
52
|
+
data-pw={typeof testId === 'string' ? testId : null}
|
|
53
|
+
testID={typeof testId === 'string' ? testId : null}
|
|
54
|
+
role="progressbar"
|
|
55
|
+
aria-valuenow={isIndeterminate ? null : preciseValueNow}
|
|
56
|
+
aria-valuemin={0}
|
|
57
|
+
aria-valuemax={100}
|
|
58
|
+
aria-valuetext={isIndeterminate ? 'indeterminate' : null}
|
|
59
|
+
aria-busy={isIndeterminate ? true : null}
|
|
60
|
+
aria-label={ariaLabel ?? labelText}
|
|
61
|
+
>
|
|
62
|
+
{#if typeof leadingLabel === 'function'}
|
|
63
|
+
{@render leadingLabel()}
|
|
64
|
+
{/if}
|
|
65
|
+
<div class="track">
|
|
66
|
+
<div
|
|
67
|
+
class="bar"
|
|
68
|
+
class:indeterminate={isIndeterminate}
|
|
69
|
+
style:width={isIndeterminate ? null : `${percentage}%`}
|
|
70
|
+
></div>
|
|
81
71
|
</div>
|
|
82
|
-
|
|
83
|
-
|
|
72
|
+
{#if showLabel && !isIndeterminate}
|
|
73
|
+
<div class="label">
|
|
74
|
+
{#if animateValue}
|
|
75
|
+
<!-- The rounded percentage is already the display value labelText
|
|
76
|
+
computes, so it is passed as a plain number rather than through
|
|
77
|
+
`format`: `AnimatedNumber`'s Intl percent style expects a 0-1
|
|
78
|
+
fraction and would re-derive a different number from this
|
|
79
|
+
already-0-100 one. The % is a literal beside it instead, exactly
|
|
80
|
+
mirroring what labelText already spells out below. -->
|
|
81
|
+
<!-- The unit goes INSIDE AnimatedNumber, not beside it. The odometer
|
|
82
|
+
carries role="img" with its own aria-label, so a unit left outside is
|
|
83
|
+
invisible to anyone navigating by graphic -- the rotor reads "75" with
|
|
84
|
+
no sign of what it measures. Passing the formatted string keeps the
|
|
85
|
+
unit in the accessible name; only the digits move, since the string
|
|
86
|
+
path rolls the characters that changed and leaves the rest. -->
|
|
87
|
+
<AnimatedNumber value={`${Math.round(percentage)}%`} />
|
|
88
|
+
{:else}
|
|
89
|
+
{labelText}
|
|
90
|
+
{/if}
|
|
91
|
+
</div>
|
|
92
|
+
{/if}
|
|
93
|
+
</div>
|
|
94
|
+
{/snippet}
|
|
95
|
+
|
|
96
|
+
{#if typeof headerStart === 'function' || typeof headerEnd === 'function' || typeof note === 'function'}
|
|
97
|
+
<div class="progress-root">
|
|
98
|
+
{#if typeof headerStart === 'function' || typeof headerEnd === 'function'}
|
|
99
|
+
<div class="progress-header">
|
|
100
|
+
{#if typeof headerStart === 'function'}{@render headerStart()}{/if}
|
|
101
|
+
{#if typeof headerEnd === 'function'}{@render headerEnd()}{/if}
|
|
102
|
+
</div>
|
|
103
|
+
{/if}
|
|
104
|
+
{@render trackRow()}
|
|
105
|
+
{#if typeof note === 'function'}
|
|
106
|
+
<div class="progress-note">{@render note()}</div>
|
|
107
|
+
{/if}
|
|
108
|
+
</div>
|
|
109
|
+
{:else}
|
|
110
|
+
{@render trackRow()}
|
|
111
|
+
{/if}
|
|
84
112
|
|
|
85
113
|
<style>
|
|
114
|
+
/* Only present when a header or note is passed; layout only, no type or
|
|
115
|
+
colour -- the snippets' content is the caller's to style. The names are prefixed so that a
|
|
116
|
+
class like `header` or `root`, which a consumer passes through `classes` to the
|
|
117
|
+
progressbar itself, cannot match them. */
|
|
118
|
+
.progress-root {
|
|
119
|
+
display: grid;
|
|
120
|
+
gap: var(--progress-root-gap, 5px);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.progress-header {
|
|
124
|
+
display: flex;
|
|
125
|
+
align-items: baseline;
|
|
126
|
+
justify-content: space-between;
|
|
127
|
+
gap: var(--progress-header-gap, 8px);
|
|
128
|
+
}
|
|
129
|
+
|
|
86
130
|
.container {
|
|
87
131
|
display: flex;
|
|
88
132
|
align-items: center;
|
|
@@ -92,13 +136,24 @@
|
|
|
92
136
|
}
|
|
93
137
|
|
|
94
138
|
.track {
|
|
95
|
-
flex: 1;
|
|
139
|
+
flex: var(--progress-track-flex, 1);
|
|
96
140
|
height: var(--progress-track-height, 8px);
|
|
97
141
|
background: var(--progress-track-background, #e0e0e0);
|
|
98
142
|
border-radius: var(--progress-track-border-radius, var(--radius, 4px));
|
|
99
143
|
overflow: hidden;
|
|
100
144
|
}
|
|
101
145
|
|
|
146
|
+
/* A track-scoped width is separate from --progress-container-width: the container also
|
|
147
|
+
holds leadingLabel/showLabel and their gaps, so sizing IT would size the whole row, not
|
|
148
|
+
the bar. Grow/shrink stay in the flex shorthand alongside the basis specifically so an
|
|
149
|
+
explicit width isn't silently stretched or shrunk away from by a consumer who set only
|
|
150
|
+
--progress-track-width and left --progress-track-flex at its default. Zero specificity,
|
|
151
|
+
so a consumer's own width rule for the track keeps winning, as it did before this hook
|
|
152
|
+
existed. */
|
|
153
|
+
:where(.track) {
|
|
154
|
+
width: var(--progress-track-width, auto);
|
|
155
|
+
}
|
|
156
|
+
|
|
102
157
|
.bar {
|
|
103
158
|
height: 100%;
|
|
104
159
|
background: var(--progress-bar-background, #2196f3);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
1
2
|
export type ProgressProperties = MandatoryProgressProperties & OptionalProgressProperties;
|
|
2
3
|
export type MandatoryProgressProperties = {
|
|
3
4
|
value: number;
|
|
@@ -11,6 +12,12 @@ export type OptionalProgressProperties = {
|
|
|
11
12
|
* `aria-valuenow` and the percentage fallback text below.
|
|
12
13
|
*/
|
|
13
14
|
max?: number;
|
|
15
|
+
/**
|
|
16
|
+
* Rendered in `.container` before the track, e.g. a fixed caption a
|
|
17
|
+
* percentage label shouldn't replace. Independent of `showLabel`, which
|
|
18
|
+
* renders after the track instead.
|
|
19
|
+
*/
|
|
20
|
+
leadingLabel?: Snippet;
|
|
14
21
|
showLabel?: boolean;
|
|
15
22
|
/**
|
|
16
23
|
* Routes the completion percentage through `AnimatedNumber` instead of
|
|
@@ -32,6 +39,18 @@ export type OptionalProgressProperties = {
|
|
|
32
39
|
* fallback reads `"0%"`.
|
|
33
40
|
*/
|
|
34
41
|
ariaLabel?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Left end of a header row above the track, e.g. the bar's name. With
|
|
44
|
+
* `headerEnd` it makes the label/value line a metered bar usually carries
|
|
45
|
+
* ("Context 42%"). Layout only: the content's type and colour are the
|
|
46
|
+
* caller's. When none of `headerStart`, `headerEnd` and `note` is passed,
|
|
47
|
+
* the component renders exactly as before, with no extra wrapper.
|
|
48
|
+
*/
|
|
49
|
+
headerStart?: Snippet;
|
|
50
|
+
/** Right end of the header row (see `headerStart`), e.g. the value text. */
|
|
51
|
+
headerEnd?: Snippet;
|
|
52
|
+
/** A line under the track, e.g. "resets in 2h" (see `headerStart`). */
|
|
53
|
+
note?: Snippet;
|
|
35
54
|
testId?: string;
|
|
36
55
|
classes?: string;
|
|
37
56
|
};
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
copyIcon,
|
|
14
14
|
copiedLabel = 'Copied!',
|
|
15
15
|
copyResetMs = 2000,
|
|
16
|
+
multiline = false,
|
|
16
17
|
oncopy,
|
|
17
18
|
onerror,
|
|
18
19
|
classes
|
|
@@ -44,11 +45,15 @@
|
|
|
44
45
|
|
|
45
46
|
<div
|
|
46
47
|
class="snippet {classes ?? ''}"
|
|
48
|
+
data-multiline={multiline ? '' : null}
|
|
49
|
+
data-has-copy={multiline && showCopyButton ? '' : null}
|
|
47
50
|
data-pw={typeof testId === 'string' ? testId : null}
|
|
48
51
|
testID={typeof testId === 'string' ? testId : null}
|
|
49
52
|
>
|
|
50
53
|
<code class="snippet-code">
|
|
51
|
-
|
|
54
|
+
{#if !multiline || prompt !== ''}
|
|
55
|
+
<span class="snippet-prompt">{prompt}</span>
|
|
56
|
+
{/if}
|
|
52
57
|
<span class="snippet-text">{text}</span>
|
|
53
58
|
</code>
|
|
54
59
|
{#if showCopyButton}
|
|
@@ -90,6 +95,10 @@
|
|
|
90
95
|
gap: var(--snippet-gap, 8px);
|
|
91
96
|
background: var(--snippet-background, #1e1e1e);
|
|
92
97
|
border: var(--snippet-border, 1px solid #333);
|
|
98
|
+
/* One side on its own, for a block that sits flush under a header and wants
|
|
99
|
+
only a separator above it. Defaults to the whole border, so unset it is
|
|
100
|
+
the shorthand above. */
|
|
101
|
+
border-top: var(--snippet-border-top, var(--snippet-border, 1px solid #333));
|
|
93
102
|
border-radius: var(--snippet-border-radius, var(--radius, 4px));
|
|
94
103
|
padding: var(--snippet-padding, 12px 16px);
|
|
95
104
|
font-family: var(--snippet-font-family, monospace);
|
|
@@ -106,6 +115,41 @@
|
|
|
106
115
|
min-width: 0;
|
|
107
116
|
}
|
|
108
117
|
|
|
118
|
+
/* multiline: every line of long output, not one ellipsised line. Declared
|
|
119
|
+
here rather than left to a consumer's own selectors, which can only TIE
|
|
120
|
+
these scoped rules on specificity -- and then stylesheet order, which
|
|
121
|
+
differs between dev and a production build, decides the layout. Marked by
|
|
122
|
+
data attributes, not classes: a class directive would strip a `multiline`
|
|
123
|
+
class a consumer already passes through `classes`, and these rules would
|
|
124
|
+
then style every snippet that carries one. */
|
|
125
|
+
.snippet[data-multiline] {
|
|
126
|
+
display: block;
|
|
127
|
+
position: relative;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.snippet[data-multiline] .snippet-code {
|
|
131
|
+
display: block;
|
|
132
|
+
overflow: auto;
|
|
133
|
+
max-height: var(--snippet-code-max-height, none);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.snippet[data-multiline][data-has-copy] .snippet-code {
|
|
137
|
+
padding-right: var(--snippet-code-padding-right, 32px);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.snippet[data-multiline] .snippet-text {
|
|
141
|
+
display: inline;
|
|
142
|
+
white-space: pre;
|
|
143
|
+
overflow: visible;
|
|
144
|
+
text-overflow: clip;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.snippet[data-multiline] .snippet-copy {
|
|
148
|
+
position: absolute;
|
|
149
|
+
top: var(--snippet-copy-inset, 8px);
|
|
150
|
+
right: var(--snippet-copy-inset, 8px);
|
|
151
|
+
}
|
|
152
|
+
|
|
109
153
|
.snippet-prompt {
|
|
110
154
|
color: var(--snippet-prompt-color, #888);
|
|
111
155
|
margin-right: var(--snippet-prompt-margin-right, 8px);
|
|
@@ -12,6 +12,14 @@ export type OptionalSnippetProperties = {
|
|
|
12
12
|
copiedLabel?: string;
|
|
13
13
|
/** Milliseconds before the copied feedback reverts to the copy icon. Defaults to `2000`. */
|
|
14
14
|
copyResetMs?: number;
|
|
15
|
+
/**
|
|
16
|
+
* Lays the snippet out as a pre-formatted block for long output (a command's
|
|
17
|
+
* stdout, a JSON dump) instead of the default one-line, ellipsised chip:
|
|
18
|
+
* every line shows, the code area scrolls (capped by
|
|
19
|
+
* `--snippet-code-max-height`), the copy button sits in the top-right corner,
|
|
20
|
+
* and an empty `prompt` renders nothing rather than an indent.
|
|
21
|
+
*/
|
|
22
|
+
multiline?: boolean;
|
|
15
23
|
classes?: string;
|
|
16
24
|
};
|
|
17
25
|
export type SnippetEventProperties = {
|
package/dist/Tabs/Tabs.svelte
CHANGED
|
@@ -17,16 +17,30 @@
|
|
|
17
17
|
activationMode = 'automatic',
|
|
18
18
|
loop = true,
|
|
19
19
|
dir,
|
|
20
|
+
navigation = false,
|
|
20
21
|
testId,
|
|
21
22
|
scrollLeftIcon,
|
|
22
23
|
scrollRightIcon,
|
|
23
24
|
tab,
|
|
24
25
|
classes,
|
|
26
|
+
ariaLabel,
|
|
25
27
|
onchange,
|
|
26
28
|
onkeychange
|
|
27
29
|
}: TabsProperties = $props();
|
|
28
30
|
|
|
29
31
|
const isVertical = $derived(orientation === 'vertical');
|
|
32
|
+
// `navigation` is passed AND every item has an href: the bar is site navigation, not a
|
|
33
|
+
// tab set. An href alone changes nothing, so items that already carry one as routing data
|
|
34
|
+
// keep rendering and behaving as the tab set they always did.
|
|
35
|
+
const isLinkMode = $derived(
|
|
36
|
+
navigation &&
|
|
37
|
+
Array.isArray(items) &&
|
|
38
|
+
items.length > 0 &&
|
|
39
|
+
items.every(
|
|
40
|
+
(item: string | TabItem) =>
|
|
41
|
+
typeof item === 'object' && typeof item.href === 'string' && item.href.length > 0
|
|
42
|
+
)
|
|
43
|
+
);
|
|
30
44
|
|
|
31
45
|
const isObjectMode = $derived(items.length > 0 && typeof items.at(0) === 'object');
|
|
32
46
|
|
|
@@ -158,7 +172,8 @@
|
|
|
158
172
|
}
|
|
159
173
|
|
|
160
174
|
function updateIndicator(): void {
|
|
161
|
-
if (scrollContainer === null) {
|
|
175
|
+
if (scrollContainer === null || isLinkMode) {
|
|
176
|
+
indicatorReady = false;
|
|
162
177
|
return;
|
|
163
178
|
}
|
|
164
179
|
const activeEl = scrollContainer.querySelector<HTMLElement>('.tabs-item.active');
|
|
@@ -230,6 +245,22 @@
|
|
|
230
245
|
activate(index);
|
|
231
246
|
}
|
|
232
247
|
|
|
248
|
+
// Link mode has no activate() of its own to consult -- the browser navigates on click, which
|
|
249
|
+
// is the point of a real <a href>. A disabled item's `aria-disabled` is cosmetic to a pointer
|
|
250
|
+
// or native Enter/Space activation, so this is the only place that actually honours
|
|
251
|
+
// TabItem.disabled's documented "clicks skip past it" for a link-mode item.
|
|
252
|
+
function blockIfDisabled(index: number, event: Event): void {
|
|
253
|
+
if (isItemDisabled(index)) {
|
|
254
|
+
event.preventDefault();
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function handleLinkKeydown(index: number, event: KeyboardEvent): void {
|
|
259
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
260
|
+
blockIfDisabled(index, event);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
233
264
|
function focusTabAt(index: number): void {
|
|
234
265
|
const item = items.at(index);
|
|
235
266
|
if (typeof item !== 'string' && typeof item !== 'object') {
|
|
@@ -433,19 +464,21 @@
|
|
|
433
464
|
{/if}
|
|
434
465
|
</button>
|
|
435
466
|
{/if}
|
|
436
|
-
<
|
|
467
|
+
<svelte:element
|
|
468
|
+
this={isLinkMode ? 'nav' : 'div'}
|
|
437
469
|
class="tabs-bar"
|
|
438
470
|
class:fade-left={canScrollLeft}
|
|
439
471
|
class:fade-right={canScrollRight}
|
|
440
472
|
class:fade-top={canScrollUp}
|
|
441
473
|
class:fade-bottom={canScrollDown}
|
|
442
|
-
role=
|
|
443
|
-
aria-orientation={isVertical ? 'vertical'
|
|
474
|
+
role={isLinkMode ? null : 'tablist'}
|
|
475
|
+
aria-orientation={isLinkMode || !isVertical ? null : 'vertical'}
|
|
476
|
+
aria-label={typeof ariaLabel === 'string' ? ariaLabel : null}
|
|
444
477
|
data-orientation={orientation}
|
|
445
478
|
{@attach initOverflow}
|
|
446
479
|
onscroll={updateOverflow}
|
|
447
|
-
onfocusin={handleFocusIn}
|
|
448
|
-
onfocusout={handleFocusOut}
|
|
480
|
+
onfocusin={isLinkMode ? null : handleFocusIn}
|
|
481
|
+
onfocusout={isLinkMode ? null : handleFocusOut}
|
|
449
482
|
>
|
|
450
483
|
{#each items as item, index (isObjectMode ? (toTabItem(item)?.key ?? index) : index)}
|
|
451
484
|
{@const tabItem = toTabItem(item)}
|
|
@@ -453,20 +486,27 @@
|
|
|
453
486
|
{#if typeof tabItem?.sectionLabel === 'string' && tabItem.sectionLabel.length > 0}
|
|
454
487
|
<div class="tabs-section-label" aria-hidden="true">{tabItem.sectionLabel}</div>
|
|
455
488
|
{/if}
|
|
456
|
-
<
|
|
489
|
+
<svelte:element
|
|
490
|
+
this={isLinkMode ? 'a' : 'div'}
|
|
491
|
+
href={isLinkMode ? tabItem?.href : null}
|
|
457
492
|
class="tabs-item"
|
|
458
493
|
class:active={isActiveItem(index)}
|
|
459
|
-
role=
|
|
460
|
-
aria-selected={isActiveItem(index)}
|
|
494
|
+
role={isLinkMode ? null : 'tab'}
|
|
495
|
+
aria-selected={isLinkMode ? null : isActiveItem(index)}
|
|
496
|
+
aria-current={isLinkMode && isActiveItem(index) ? 'page' : null}
|
|
461
497
|
aria-disabled={isItemDisabled(index) ? 'true' : null}
|
|
462
|
-
tabindex={index === tabStopIndex ? 0 : -1}
|
|
498
|
+
tabindex={isLinkMode ? null : index === tabStopIndex ? 0 : -1}
|
|
463
499
|
data-state={isActiveItem(index) ? 'active' : 'inactive'}
|
|
464
500
|
data-disabled={isItemDisabled(index) ? '' : null}
|
|
465
501
|
data-orientation={orientation}
|
|
466
502
|
data-pw={tabItem?.testId}
|
|
467
503
|
testID={tabItem?.testId}
|
|
468
|
-
onclick={
|
|
469
|
-
|
|
504
|
+
onclick={isLinkMode
|
|
505
|
+
? (event: MouseEvent) => blockIfDisabled(index, event)
|
|
506
|
+
: () => handleTabClick(index)}
|
|
507
|
+
onkeydown={isLinkMode
|
|
508
|
+
? (event: KeyboardEvent) => handleLinkKeydown(index, event)
|
|
509
|
+
: (event: KeyboardEvent) => handleKeydown(event, index)}
|
|
470
510
|
>
|
|
471
511
|
{#if typeof tab === 'function'}
|
|
472
512
|
{@render tab({
|
|
@@ -486,9 +526,9 @@
|
|
|
486
526
|
<span class="tabs-item-status status-{tabItem.status}" aria-hidden="true"></span>
|
|
487
527
|
{/if}
|
|
488
528
|
{/if}
|
|
489
|
-
</
|
|
529
|
+
</svelte:element>
|
|
490
530
|
{/each}
|
|
491
|
-
{#if indicatorReady}
|
|
531
|
+
{#if indicatorReady && !isLinkMode}
|
|
492
532
|
<span
|
|
493
533
|
class="tabs-indicator"
|
|
494
534
|
aria-hidden="true"
|
|
@@ -497,7 +537,7 @@
|
|
|
497
537
|
: `left: ${indicatorLeft}px; width: ${indicatorWidth}px;`}
|
|
498
538
|
></span>
|
|
499
539
|
{/if}
|
|
500
|
-
</
|
|
540
|
+
</svelte:element>
|
|
501
541
|
{#if showEndArrow}
|
|
502
542
|
<button
|
|
503
543
|
class="tabs-arrow tabs-arrow-end"
|
|
@@ -638,7 +678,7 @@
|
|
|
638
678
|
border-radius: var(--tabs-item-border-radius, 0);
|
|
639
679
|
outline: none;
|
|
640
680
|
white-space: nowrap;
|
|
641
|
-
flex-shrink: 0;
|
|
681
|
+
flex-shrink: var(--tabs-item-flex-shrink, 0);
|
|
642
682
|
user-select: none;
|
|
643
683
|
transition: var(
|
|
644
684
|
--tabs-transition,
|
|
@@ -649,6 +689,19 @@
|
|
|
649
689
|
);
|
|
650
690
|
}
|
|
651
691
|
|
|
692
|
+
/* The grow and basis hooks live in a zero-specificity rule, so a consumer's own rule for
|
|
693
|
+
either (`.tabs-item { flex: 1 }` to stretch the items) wins exactly as it did before the
|
|
694
|
+
hooks existed. The shrink hook above replaces the literal that was already there. */
|
|
695
|
+
:where(.tabs-item) {
|
|
696
|
+
flex-grow: var(--tabs-item-flex-grow, 0);
|
|
697
|
+
flex-basis: var(--tabs-item-flex-basis, auto);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/* A link-mode item is an <a>: no underline unless asked for. Only an anchor matches. */
|
|
701
|
+
a.tabs-item {
|
|
702
|
+
text-decoration: var(--tabs-item-text-decoration, none);
|
|
703
|
+
}
|
|
704
|
+
|
|
652
705
|
/* Roving focus really does move (see focusedKey/focusTabAt above) -- this
|
|
653
706
|
just replaces the INDICATOR that outline:none above strips, which is what
|
|
654
707
|
was actually missing (WCAG 2.4.7). :focus-visible only, so a mouse click
|
|
@@ -850,6 +903,12 @@
|
|
|
850
903
|
user-select: none;
|
|
851
904
|
}
|
|
852
905
|
|
|
906
|
+
/* Zero specificity, so a consumer's own font rule for the label still wins. */
|
|
907
|
+
:where(.tabs-section-label) {
|
|
908
|
+
font-family: var(--tabs-section-label-font-family, inherit);
|
|
909
|
+
font-variant-ligatures: var(--tabs-section-label-font-variant-ligatures, inherit);
|
|
910
|
+
}
|
|
911
|
+
|
|
853
912
|
@media (prefers-reduced-motion: reduce) {
|
|
854
913
|
.tabs-indicator,
|
|
855
914
|
.tabs-wrapper.vertical .tabs-indicator {
|
|
@@ -23,6 +23,12 @@ export type TabItem = {
|
|
|
23
23
|
* `tab` snippet, since it sits outside the item row.
|
|
24
24
|
*/
|
|
25
25
|
sectionLabel?: string;
|
|
26
|
+
/**
|
|
27
|
+
* The link target in navigation mode (see `navigation` on the bar). Ignored
|
|
28
|
+
* unless `navigation` is set, so an item that already carries an `href` as its
|
|
29
|
+
* own routing data renders and behaves exactly as before.
|
|
30
|
+
*/
|
|
31
|
+
href?: string;
|
|
26
32
|
/**
|
|
27
33
|
* Blocks selection of this item. It keeps its place and its `aria-disabled`
|
|
28
34
|
* state, but arrow keys, `Home`/`End` and clicks skip past it, and it never
|
|
@@ -36,6 +42,21 @@ export type MandatoryTabsProperties = {
|
|
|
36
42
|
items: string[] | TabItem[];
|
|
37
43
|
};
|
|
38
44
|
export type OptionalTabsProperties = {
|
|
45
|
+
/**
|
|
46
|
+
* Turns the bar into site navigation instead of a tab set, when EVERY item also
|
|
47
|
+
* has an `href`: a `<nav>` of `<a href>` items, the active one marked
|
|
48
|
+
* `aria-current="page"`, reached with Tab like any links -- no tablist/tab
|
|
49
|
+
* roles, no roving tabindex, no arrow keys, no sliding indicator. Navigation is
|
|
50
|
+
* the browser's; `onchange` does not fire. Defaults to false, which keeps the
|
|
51
|
+
* tab set whatever the items contain.
|
|
52
|
+
*/
|
|
53
|
+
navigation?: boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Accessible name for the bar -- the tablist, or the `<nav>` in link mode
|
|
56
|
+
* (e.g. "Manual controls"; a `sectionLabel` is aria-hidden, so it does not
|
|
57
|
+
* name the bar).
|
|
58
|
+
*/
|
|
59
|
+
ariaLabel?: string;
|
|
39
60
|
activeIndex?: number;
|
|
40
61
|
activeKey?: string;
|
|
41
62
|
disabled?: boolean;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { ThinkingIndicatorProperties } from './properties';
|
|
2
|
-
declare const ThinkingIndicator: import("svelte").Component<ThinkingIndicatorProperties, {}, "
|
|
2
|
+
declare const ThinkingIndicator: import("svelte").Component<ThinkingIndicatorProperties, {}, "expanded" | "selected">;
|
|
3
3
|
type ThinkingIndicator = ReturnType<typeof ThinkingIndicator>;
|
|
4
4
|
export default ThinkingIndicator;
|