@juspay/svelte-ui-components 2.134.0 → 2.135.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.
@@ -3,7 +3,7 @@
3
3
  import ChatMessageList from '../ChatMessageList/ChatMessageList.svelte';
4
4
  import ChatComposer from '../ChatComposer/ChatComposer.svelte';
5
5
  import ChatSuggestions from '../ChatSuggestions/ChatSuggestions.svelte';
6
- import ChatToolStatus from '../ChatToolStatus/ChatToolStatus.svelte';
6
+ import ThinkingIndicator from '../ThinkingIndicator/ThinkingIndicator.svelte';
7
7
  import type { ChatProperties } from './properties';
8
8
 
9
9
  let {
@@ -106,7 +106,7 @@
106
106
  <ChatSuggestions items={suggestions} {disabled} onselect={handleSuggestion} />
107
107
  {/if}
108
108
  {#if toolStatus !== null}
109
- <div class="tool-status"><ChatToolStatus label={toolStatus.label} /></div>
109
+ <div class="tool-status"><ThinkingIndicator label={toolStatus.label} variant="chip" /></div>
110
110
  {/if}
111
111
  <ChatComposer
112
112
  bind:value
@@ -156,5 +156,29 @@
156
156
  .tool-status {
157
157
  display: flex;
158
158
  justify-content: var(--chat-tool-status-justify, center);
159
+ /* This row is ThinkingIndicator's chip variant internally now, not the standalone
160
+ ChatToolStatus component -- but its public theming contract stays
161
+ --chat-tool-status-*, unchanged, so an existing consumer's overrides keep working
162
+ exactly as before. Defaults below match ChatToolStatus's own byte for byte. */
163
+ --thinking-indicator-chip-gap: var(--chat-tool-status-gap, 8px);
164
+ --thinking-indicator-chip-padding: var(--chat-tool-status-padding, 8px 14px);
165
+ --thinking-indicator-chip-background: var(--chat-tool-status-background, #ffffff);
166
+ --thinking-indicator-chip-border: var(--chat-tool-status-border, 1px solid #e4e4e7);
167
+ --thinking-indicator-chip-border-radius: var(--chat-tool-status-border-radius, 999px);
168
+ --thinking-indicator-chip-box-shadow: var(
169
+ --chat-tool-status-box-shadow,
170
+ 0 6px 20px rgba(0, 0, 0, 0.08)
171
+ );
172
+ --thinking-indicator-chip-max-width: var(--chat-tool-status-max-width, 100%);
173
+ --thinking-indicator-chip-color: var(--chat-tool-status-color, #52525b);
174
+ --thinking-indicator-chip-font-size: var(--chat-tool-status-font-size, 0.85rem);
175
+ --thinking-indicator-chip-font-weight: var(--chat-tool-status-font-weight, 500);
176
+ --thinking-indicator-chip-icon-color: var(--chat-tool-status-indicator-color, currentColor);
177
+ --thinking-indicator-chip-spinner-color: var(--chat-tool-status-spinner-color, currentColor);
178
+ --thinking-indicator-chip-spinner-color-end: var(
179
+ --chat-tool-status-spinner-color-end,
180
+ transparent
181
+ );
182
+ --thinking-indicator-chip-spinner-size: var(--chat-tool-status-spinner-size, 14px);
159
183
  }
160
184
  </style>
@@ -5,6 +5,7 @@
5
5
 
6
6
  let {
7
7
  values = $bindable([]),
8
+ ariaLabel,
8
9
  placeholder = 'Add value…',
9
10
  disabled = false,
10
11
  testId,
@@ -66,6 +67,7 @@
66
67
  {placeholder}
67
68
  dataType="text"
68
69
  name=""
70
+ {ariaLabel}
69
71
  autoComplete="off"
70
72
  actionInput={false}
71
73
  disable={disabled}
@@ -9,6 +9,12 @@ export type MandatoryChipInputProperties = {
9
9
  values: string[];
10
10
  };
11
11
  export type OptionalChipInputProperties = {
12
+ /**
13
+ * Names the draft field for assistive technology. ChipInput renders no visible label of its
14
+ * own, so without this the control reaches the accessibility tree unnamed however the caption
15
+ * beside it reads on screen. Pass the same words as that caption.
16
+ */
17
+ ariaLabel?: string;
12
18
  placeholder?: string;
13
19
  disabled?: boolean;
14
20
  testId?: string;
@@ -122,6 +122,26 @@
122
122
  // forceError lets consumers drive the error border from server/runtime validation,
123
123
  // independent of validationPattern.
124
124
  const showError = $derived(showErrorMessage || forceError);
125
+ // The error text has to be reachable FROM the field it describes and announced when it
126
+ // appears. Without both, a screen-reader user submits, hears nothing, and is left on a form
127
+ // that did not move -- the message is on screen and absent from the accessibility tree.
128
+ const errorMessageId = $derived(`${effectiveId}-error`);
129
+ // `onErrorMessage` is `string | null`, so null has to be excluded explicitly: `null !== ''`
130
+ // is true, which would describe the field by an alert element carrying no message.
131
+ const isShowingError = $derived(
132
+ onErrorMessage != null && onErrorMessage !== '' && showError && !actionInput
133
+ );
134
+ const infoMessageId = $derived(`${effectiveId}-info`);
135
+ const isShowingInfo = $derived(infoMessage !== '' && !actionInput);
136
+ // Helper text is part of the field's description, not decoration: a consumer's `infoMessage`
137
+ // ("Enter a percentage between 1 and 100") is exactly the guidance a screen-reader user needs
138
+ // BEFORE they trip an error. Reference both, in reading order, so the field is described by
139
+ // everything visibly attached to it rather than only by its failure.
140
+ const describedBy = $derived(
141
+ [isShowingError ? errorMessageId : null, isShowingInfo ? infoMessageId : null]
142
+ .filter(Boolean)
143
+ .join(' ')
144
+ );
125
145
  const hasLeftIcon = $derived(typeof leftIcon === 'function');
126
146
  const hasRightIcon = $derived(typeof rightIcon === 'function');
127
147
 
@@ -301,6 +321,8 @@
301
321
  aria-controls={ariaControls}
302
322
  aria-activedescendant={ariaActivedescendant}
303
323
  aria-required={mandatory || null}
324
+ aria-invalid={showError ? 'true' : null}
325
+ aria-describedby={describedBy || null}
304
326
  required={mandatory || null}
305
327
  onfocus={onFocus}
306
328
  onfocusout={_onFocusOut}
@@ -337,6 +359,8 @@
337
359
  aria-controls={ariaControls}
338
360
  aria-activedescendant={ariaActivedescendant}
339
361
  aria-required={mandatory || null}
362
+ aria-invalid={showError ? 'true' : null}
363
+ aria-describedby={describedBy || null}
340
364
  required={mandatory || null}
341
365
  onfocus={onFocus}
342
366
  onfocusout={_onFocusOut}
@@ -399,8 +423,10 @@
399
423
  {@render fieldElement()}
400
424
  {/if}
401
425
 
402
- {#if onErrorMessage !== '' && showError && !actionInput}
426
+ {#if isShowingError}
403
427
  <div
428
+ id={errorMessageId}
429
+ role="alert"
404
430
  class="error-message"
405
431
  data-pw={typeof testId === 'string' && testId.length > 0 ? `${testId}-error-message` : null}
406
432
  testID={typeof testId === 'string' && testId.length > 0 ? `${testId}-error-message` : null}
@@ -408,8 +434,8 @@
408
434
  {onErrorMessage}
409
435
  </div>
410
436
  {/if}
411
- {#if infoMessage !== '' && !actionInput}
412
- <div class="info-message">
437
+ {#if isShowingInfo}
438
+ <div id={infoMessageId} class="info-message">
413
439
  {infoMessage}
414
440
  </div>
415
441
  {/if}
@@ -40,16 +40,20 @@
40
40
 
41
41
  // A detail string or a trace is what makes the indicator expandable. Without either
42
42
  // there is nothing to reveal, so it renders as a plain live status line instead of a
43
- // disclosure control. `bare` overrides that entirely.
43
+ // disclosure control. `bare` and `chip` both override that entirely — neither has
44
+ // room (or a documented reason) to expand.
44
45
  const isExpandable = $derived(
45
- variant !== 'bare' && (hasTraceMode || (typeof detail === 'string' && detail.length > 0))
46
+ variant === 'default' && (hasTraceMode || (typeof detail === 'string' && detail.length > 0))
46
47
  );
47
48
 
48
49
  // Backward-compatible shimmer rule: without an explicit `busy`, the expandable
49
50
  // summary holds still (settled) and every other shape shimmers (live) — exactly
50
- // today's released behaviour. Passing `busy` takes direct control of the shimmer
51
- // in every shape, including `bare`.
52
- const labelIsBusy = $derived(busy ?? !isExpandable);
51
+ // today's released behaviour. `chip` is the one exception, defaulting to static:
52
+ // it exists to be a drop-in for ChatToolStatus, which never shimmered, so a
53
+ // caller porting that usage 1:1 shouldn't have to also learn `busy={false}` just
54
+ // to avoid an unexpected shimmer. Passing `busy` takes direct control of the
55
+ // shimmer in every shape, including `bare` and `chip`.
56
+ const labelIsBusy = $derived(busy ?? (variant === 'chip' ? false : !isExpandable));
53
57
 
54
58
  // Set once a person clicks the disclosure open/closed — from then on the automatic
55
59
  // busy-driven machine below leaves `expanded` alone for the rest of this mount.
@@ -406,6 +410,21 @@
406
410
  >{label}</span
407
411
  >{:else}{label}{/if}</span
408
412
  >
413
+ {:else if variant === 'chip'}
414
+ <div
415
+ class="thinking-indicator-chip {classes ?? ''}"
416
+ aria-live="polite"
417
+ aria-atomic="true"
418
+ data-pw={typeof testId === 'string' ? testId : null}
419
+ testID={typeof testId === 'string' ? testId : null}
420
+ >
421
+ <span class="chip-icon">
422
+ {#if avatar}{@render avatar()}{:else}<Loader />{/if}
423
+ </span>
424
+ <span class="chip-label" class:static-label={!labelIsBusy} data-pw={labelTestId ?? null}
425
+ >{label}</span
426
+ >
427
+ </div>
409
428
  {:else}
410
429
  <div
411
430
  class="thinking-indicator status-host {classes ?? ''}"
@@ -558,6 +577,69 @@
558
577
  -webkit-text-fill-color: var(--thinking-indicator-label-color, #858585);
559
578
  }
560
579
 
580
+ /* ---- chip variant: a self-contained floating pill (no ancestor supplies
581
+ background/layout, unlike bare) — deliberately not `width: 100%`/box-sizing
582
+ from `.thinking-indicator`, since a pill hugs its content. */
583
+ .thinking-indicator-chip {
584
+ box-sizing: border-box;
585
+ display: inline-flex;
586
+ align-items: center;
587
+ gap: var(--thinking-indicator-chip-gap, 8px);
588
+ width: fit-content;
589
+ padding: var(--thinking-indicator-chip-padding, 8px 14px);
590
+ background: var(--thinking-indicator-chip-background, #ffffff);
591
+ border: var(--thinking-indicator-chip-border, 1px solid #e4e4e7);
592
+ border-radius: var(--thinking-indicator-chip-border-radius, 999px);
593
+ box-shadow: var(--thinking-indicator-chip-box-shadow, 0 6px 20px rgba(0, 0, 0, 0.08));
594
+ max-width: var(--thinking-indicator-chip-max-width, 100%);
595
+ }
596
+
597
+ .chip-icon {
598
+ display: inline-flex;
599
+ align-items: center;
600
+ flex-shrink: 0;
601
+ color: var(--thinking-indicator-chip-icon-color, currentColor);
602
+ --loader-foreground: var(--thinking-indicator-chip-spinner-color, currentColor);
603
+ --loader-foreground-end: var(--thinking-indicator-chip-spinner-color-end, transparent);
604
+ --loader-width: var(--thinking-indicator-chip-spinner-size, 14px);
605
+ --loader-height: var(--thinking-indicator-chip-spinner-size, 14px);
606
+ --loader-before-width: 7px;
607
+ --loader-before-height: 7px;
608
+ --loader-after-width: 11px;
609
+ --loader-after-height: 11px;
610
+ --loader-background: var(--thinking-indicator-chip-background, #ffffff);
611
+ }
612
+
613
+ .chip-label {
614
+ flex: 0 1 auto;
615
+ min-width: 0;
616
+ white-space: nowrap;
617
+ overflow: hidden;
618
+ text-overflow: ellipsis;
619
+ font-size: var(--thinking-indicator-chip-font-size, 0.85rem);
620
+ font-weight: var(--thinking-indicator-chip-font-weight, 500);
621
+ color: var(--thinking-indicator-chip-color, #52525b);
622
+ background: var(
623
+ --thinking-indicator-chip-shimmer-gradient,
624
+ linear-gradient(90deg, #52525b 0%, #a0a0a0 50%, #52525b 100%)
625
+ );
626
+ background-size: 200% 100%;
627
+ background-clip: text;
628
+ -webkit-text-fill-color: transparent;
629
+ animation: thinking-indicator-shimmer var(--thinking-indicator-chip-shimmer-duration, 2s) linear
630
+ infinite;
631
+ }
632
+
633
+ /* Chip defaults to a static label (unlike the default/bare variants) since it
634
+ mirrors ChatToolStatus, which never shimmered — pass `busy` explicitly to
635
+ opt in. Higher specificity than the generic `.static-label` rule above, so
636
+ it correctly overrides with the chip's own color token, not the default
637
+ variant's. */
638
+ .chip-label.static-label {
639
+ animation: none;
640
+ -webkit-text-fill-color: var(--thinking-indicator-chip-color, #52525b);
641
+ }
642
+
561
643
  .arrow {
562
644
  display: inline-flex;
563
645
  align-items: center;
@@ -784,6 +866,7 @@
784
866
 
785
867
  @media (prefers-reduced-motion: reduce) {
786
868
  .status-label,
869
+ .chip-label,
787
870
  .trace-row,
788
871
  .trace-more {
789
872
  animation-duration: 0.001s;
@@ -799,5 +882,10 @@
799
882
  animation: none;
800
883
  -webkit-text-fill-color: var(--thinking-indicator-label-color, #858585);
801
884
  }
885
+
886
+ .chip-label {
887
+ animation: none;
888
+ -webkit-text-fill-color: var(--thinking-indicator-chip-color, #52525b);
889
+ }
802
890
  }
803
891
  </style>
@@ -1,5 +1,5 @@
1
1
  import type { Snippet } from 'svelte';
2
- export type ThinkingIndicatorVariant = 'default' | 'bare';
2
+ export type ThinkingIndicatorVariant = 'default' | 'bare' | 'chip';
3
3
  export type ThinkingIndicatorKind = 'steps' | 'reasoning' | 'search' | 'coding';
4
4
  export type ThinkingIndicatorTraceRow = {
5
5
  /** The step, sentence, source title, or file action. */
@@ -29,14 +29,18 @@ export type OptionalThinkingIndicatorProperties = {
29
29
  expanded?: boolean;
30
30
  /**
31
31
  * `bare` renders only the shimmering label — for chat bubbles where the surrounding
32
- * UI already supplies the avatar and layout. It never becomes expandable.
32
+ * UI already supplies the avatar and layout. `chip` renders a self-contained pill
33
+ * (bordered, shadowed, its own background) for a live status floating above other
34
+ * content, e.g. a tool-call indicator above a composer — pass `busy={false}` for a
35
+ * static (non-shimmering) label, matching a plain status badge. Neither `bare` nor
36
+ * `chip` ever becomes expandable.
33
37
  */
34
38
  variant?: ThinkingIndicatorVariant;
35
39
  /**
36
40
  * Renders an elapsed `Ns` counter while the label is live. Starts at 0 when a busy
37
41
  * phase begins, ticks every second, and freezes at its final value once the label
38
42
  * settles (driven by `busy` when set, otherwise by the legacy status-line/detail
39
- * shape). No effect on the `bare` variant.
43
+ * shape). No effect on the `bare` or `chip` variants.
40
44
  */
41
45
  showElapsed?: boolean;
42
46
  onToggle?: () => void;