@juspay/svelte-ui-components 2.125.0 → 2.127.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.
@@ -1,4 +1,5 @@
1
1
  import type { Snippet } from 'svelte';
2
+ import type { AttachmentChipFile, AttachmentChipImage, AttachmentChipVideo } from '../AttachmentChipRow/properties';
2
3
  export type ChatComposerProperties = OptionalChatComposerProperties & ChatComposerEventProperties;
3
4
  export type OptionalChatComposerProperties = {
4
5
  value?: string;
@@ -9,6 +10,38 @@ export type OptionalChatComposerProperties = {
9
10
  streaming?: boolean;
10
11
  recording?: boolean;
11
12
  attachments?: File[];
13
+ /**
14
+ * Replaces the built-in attachment pill strip above the input row — render a
15
+ * richer preview there (e.g. AttachmentChipRow) driven by your own attachment
16
+ * model. When your model lives outside `attachments`, pair with `sendable` so
17
+ * attachment-only sends stay possible.
18
+ */
19
+ attachmentsPreview?: Snippet;
20
+ /**
21
+ * Processed image attachments for the built-in rich strip: when either rich
22
+ * list is non-empty the composer renders an AttachmentChipRow above the input
23
+ * row instead of the `attachments` pill strip, and counts the chips toward
24
+ * "can send". Opt-in — with both lists empty (the default) nothing changes.
25
+ * `attachmentsPreview` still replaces the strip entirely when provided.
26
+ */
27
+ richImages?: AttachmentChipImage[];
28
+ /** Processed file attachments for the built-in rich strip — see `richImages`. */
29
+ richFiles?: AttachmentChipFile[];
30
+ /** Processed video attachments for the built-in rich strip — see `richImages`. */
31
+ richVideos?: AttachmentChipVideo[];
32
+ /** Tooltip text for a rich image chip. Nothing is shown when omitted or empty. */
33
+ richImageTooltip?: (image: AttachmentChipImage) => string;
34
+ /** Tooltip text for a rich video chip. Nothing is shown when omitted or empty. */
35
+ richVideoTooltip?: (video: AttachmentChipVideo) => string;
36
+ /** Glyph inside the rich chips' remove buttons. Falls back to the chip row's cross. */
37
+ richRemoveIcon?: Snippet;
38
+ /** Glyph on a rich file chip. Falls back to the chip row's document glyph. */
39
+ richFileIcon?: Snippet;
40
+ /**
41
+ * Overrides the internal "can send" calculation (text or `attachments`
42
+ * present). `null` keeps the default. `disabled` still wins.
43
+ */
44
+ sendable?: boolean | null;
12
45
  accept?: string;
13
46
  multiple?: boolean;
14
47
  sendLabel?: string;
@@ -19,15 +52,58 @@ export type OptionalChatComposerProperties = {
19
52
  stopIcon?: Snippet;
20
53
  voiceIcon?: Snippet;
21
54
  attachIcon?: Snippet;
55
+ /** Glyph for the idle action button (see `onaction`). */
56
+ actionIcon?: Snippet;
57
+ actionLabel?: string;
22
58
  leading?: Snippet;
23
59
  testId?: string;
60
+ /** Test id emitted as `data-pw` on the textarea itself (none by default). */
61
+ inputTestId?: string;
62
+ /** Accessible name for the textarea. Defaults to the placeholder (or 'Message'). */
63
+ inputAriaLabel?: string;
64
+ /** Test ids for the control buttons (none by default). */
65
+ sendTestId?: string;
66
+ /** Test id on the send slot's wrapper — the same slot renders send, stop and action. */
67
+ sendSlotTestId?: string;
68
+ stopTestId?: string;
69
+ voiceTestId?: string;
70
+ attachTestId?: string;
71
+ actionTestId?: string;
24
72
  classes?: string;
25
73
  };
26
74
  export type ChatComposerEventProperties = {
27
75
  onsubmit?: (value: string, attachments: File[]) => void;
28
76
  oninput?: (value: string, event: Event) => void;
29
77
  onkeydown?: (event: KeyboardEvent) => void;
78
+ onpaste?: (event: ClipboardEvent) => void;
30
79
  onstop?: () => void;
31
80
  onvoice?: () => void;
32
81
  onattach?: (files: File[]) => void;
82
+ /**
83
+ * Intercepts the attach button: when provided, clicking it calls this instead of
84
+ * opening the built-in file picker — for apps with their own chooser (camera /
85
+ * gallery / files).
86
+ */
87
+ onattachclick?: () => void;
88
+ /**
89
+ * Removal callbacks for the rich strip's chips. Omit them to render the rich
90
+ * chips read-only (no remove buttons), mirroring AttachmentChipRow.
91
+ */
92
+ onremoverichimage?: (id: string) => void;
93
+ onremoverichfile?: (id: string) => void;
94
+ onremoverichvideo?: (id: string) => void;
95
+ /**
96
+ * Open/preview callbacks for the rich strip's chips — when provided, a chip's
97
+ * tile becomes a real button and clicking it fires with the attachment (e.g.
98
+ * open a lightbox, play the video).
99
+ */
100
+ onopenrichimage?: (image: AttachmentChipImage) => void;
101
+ onopenrichvideo?: (video: AttachmentChipVideo) => void;
102
+ onopenrichfile?: (file: AttachmentChipFile) => void;
103
+ /**
104
+ * The composer's idle action (e.g. voice conversation mode). While the value is
105
+ * empty, nothing is sendable and no reply is streaming, the send button gives way
106
+ * to an action button that fires this.
107
+ */
108
+ onaction?: () => void;
33
109
  };
@@ -1,31 +1,100 @@
1
1
  <script lang="ts">
2
2
  import Pill from '../Pill/Pill.svelte';
3
+ import Img from '../Img/Img.svelte';
4
+ import Scroller from '../Scroller/Scroller.svelte';
3
5
  import type { ChatSuggestion, ChatSuggestionsProperties } from './properties';
4
6
 
5
- let { items, disabled = false, onselect, testId, classes }: ChatSuggestionsProperties = $props();
7
+ let {
8
+ items,
9
+ disabled = false,
10
+ layout = 'wrap',
11
+ direction = 'horizontal',
12
+ maxVisible,
13
+ loading = false,
14
+ icon,
15
+ chipClasses,
16
+ onselect,
17
+ testId,
18
+ classes
19
+ }: ChatSuggestionsProperties = $props();
6
20
 
7
- function labelOf(item: ChatSuggestion): string {
8
- return typeof item === 'string' ? item : item.label;
9
- }
21
+ const labelOf = (item: ChatSuggestion): string => (typeof item === 'string' ? item : item.label);
10
22
 
11
- function valueOf(item: ChatSuggestion): string {
23
+ const valueOf = (item: ChatSuggestion): string => {
12
24
  if (typeof item === 'string') {
13
25
  return item;
14
26
  }
15
27
  return item.value ?? item.label;
16
- }
28
+ };
29
+
30
+ const iconOf = (item: ChatSuggestion): string | null =>
31
+ typeof item === 'string' ? null : (item.icon ?? null);
32
+
33
+ // The dispatched value is usually a longer phrasing of the label, so it doubles as
34
+ // the hover text. Suppressed when they are the same, to avoid a tooltip that just
35
+ // repeats the chip.
36
+ const hintOf = (item: ChatSuggestion): string | null => {
37
+ if (typeof item === 'string') {
38
+ return null;
39
+ }
40
+ if (typeof item.hint === 'string') {
41
+ return item.hint;
42
+ }
43
+ return typeof item.value === 'string' && item.value !== item.label ? item.value : null;
44
+ };
45
+
46
+ // Clamped so a negative limit means "show nothing", not slice-from-the-end.
47
+ let shown = $derived(
48
+ typeof maxVisible === 'number' ? items.slice(0, Math.max(0, maxVisible)) : items
49
+ );
17
50
  </script>
18
51
 
52
+ {#snippet chips()}
53
+ {#each shown as item, index (index)}
54
+ {@const resolvedIcon = iconOf(item)}
55
+ <button
56
+ type="button"
57
+ class="chip {chipClasses ?? ''}"
58
+ title={hintOf(item)}
59
+ {disabled}
60
+ onclick={() => onselect?.(valueOf(item), index)}
61
+ >
62
+ <Pill text={labelOf(item)} {disabled}>
63
+ {#snippet leadingIcon()}
64
+ {#if typeof icon === 'function'}
65
+ {@render icon(resolvedIcon, index)}
66
+ {:else if typeof resolvedIcon === 'string'}
67
+ <Img src={resolvedIcon} alt="" fallback="" inlineSvg={true} />
68
+ {/if}
69
+ {/snippet}
70
+ </Pill>
71
+ </button>
72
+ {/each}
73
+ {/snippet}
74
+
19
75
  <div
20
76
  class="chat-suggestions {classes ?? ''}"
77
+ class:vertical={direction === 'vertical'}
78
+ class:loading
21
79
  data-pw={typeof testId === 'string' ? testId : null}
22
80
  testID={typeof testId === 'string' ? testId : null}
23
81
  >
24
- {#each items as item, index (index)}
25
- <div class="chip">
26
- <Pill text={labelOf(item)} {disabled} onclick={() => onselect?.(valueOf(item), index)} />
27
- </div>
28
- {/each}
82
+ {#if !loading}
83
+ {#if layout === 'scroll'}
84
+ <Scroller
85
+ {direction}
86
+ showArrows={false}
87
+ showGradient={false}
88
+ hideScrollbar={true}
89
+ dragToScroll={true}
90
+ classes="chat-suggestions-scroller"
91
+ >
92
+ {@render chips()}
93
+ </Scroller>
94
+ {:else}
95
+ {@render chips()}
96
+ {/if}
97
+ {/if}
29
98
  </div>
30
99
 
31
100
  <style>
@@ -39,8 +108,46 @@
39
108
  padding: var(--chat-suggestions-padding, 0);
40
109
  }
41
110
 
111
+ /* A vertical stack is a menu, not a chip row: each entry takes the full width so
112
+ the labels align, which is what makes a long list scannable. */
113
+ .chat-suggestions.vertical {
114
+ flex-direction: column;
115
+ align-items: var(--chat-suggestions-vertical-align-items, stretch);
116
+ }
117
+
118
+ .chat-suggestions.vertical .chip {
119
+ width: var(--chat-suggestions-vertical-chip-width, 100%);
120
+ --pill-width: 100%;
121
+ --pill-justify-content: flex-start;
122
+ --pill-text-align: left;
123
+ }
124
+
42
125
  .chip {
126
+ appearance: none;
127
+ padding: 0;
128
+ border: 0;
129
+ background: none;
130
+ font: inherit;
131
+ color: inherit;
132
+ text-align: inherit;
133
+ cursor: pointer;
43
134
  display: flex;
135
+ /* A chip must never be wider than the row that holds it. Pills are content-sized
136
+ and never wrap their label, so a long one spilled past the right edge —
137
+ measured, four chips overflowed an 80px container.
138
+ max-width alone is not enough: a flex item's min-width is auto, so it refuses
139
+ to shrink below its content. min-width:0 lets it, and capping --pill-max-width
140
+ hands the overflow to Pill's own ellipsis. */
141
+ min-width: 0;
142
+ max-width: 100%;
143
+ --pill-max-width: 100%;
144
+ --pill-flex-shrink: 1;
44
145
  --pill-cursor: pointer;
45
146
  }
147
+
148
+ /* The Pill is itself a flex item, and its own min-width:auto is the last thing
149
+ holding the label at full width. Zeroing it is what actually lets the ellipsis run. */
150
+ .chip > :global(.pill) {
151
+ min-width: 0;
152
+ }
46
153
  </style>
@@ -1,15 +1,57 @@
1
+ import type { Snippet } from 'svelte';
2
+ /**
3
+ * A suggestion chip.
4
+ *
5
+ * `label` is what the merchant reads; `value` is what gets dispatched. They are
6
+ * separate because a short call-to-action ("Refund trends") usually stands in for
7
+ * a much longer query, and sending the label would send the wrong thing.
8
+ */
1
9
  export type ChatSuggestion = string | {
2
10
  label: string;
3
11
  value?: string;
12
+ /** Leading mark for the chip — an image or SVG *URL* (rendered via Img with
13
+ * inlineSvg). For raw markup or custom rendering, use the `icon` snippet. */
14
+ icon?: string;
15
+ /** Hover/long-press text. Defaults to `value` when that differs from `label`. */
16
+ hint?: string;
4
17
  };
18
+ /**
19
+ * How the chips are arranged.
20
+ *
21
+ * `wrap` flows them onto as many lines as needed — right for a roomy panel.
22
+ * `scroll` keeps them on one line inside a draggable scroller — right for a
23
+ * composer on a phone, where wrapping would push the input off-screen.
24
+ */
25
+ export type ChatSuggestionsLayout = 'wrap' | 'scroll';
26
+ export type ChatSuggestionsDirection = 'horizontal' | 'vertical';
5
27
  export type ChatSuggestionsProperties = OptionalChatSuggestionsProperties & ChatSuggestionsEventProperties & MandatoryChatSuggestionsProperties;
6
28
  export type MandatoryChatSuggestionsProperties = {
7
29
  items: ChatSuggestion[];
8
30
  };
9
31
  export type OptionalChatSuggestionsProperties = {
10
32
  disabled?: boolean;
33
+ layout?: ChatSuggestionsLayout;
34
+ direction?: ChatSuggestionsDirection;
35
+ /** Render at most this many chips. Omit to render every item. */
36
+ maxVisible?: number;
37
+ /**
38
+ * Suppresses the whole row without unmounting it — for the window between asking
39
+ * and answering, where stale suggestions would invite a second wrong question.
40
+ */
41
+ loading?: boolean;
42
+ /** Custom mark per chip; receives the item's resolved icon string, or null. */
43
+ icon?: Snippet<[string | null, number]>;
11
44
  testId?: string;
12
45
  classes?: string;
46
+ /**
47
+ * Class string placed on EVERY chip wrapper.
48
+ *
49
+ * A consuming app usually keeps its chip appearance in a central stylesheet keyed on
50
+ * its own class name. Without a hook the library's own wrapper is the only thing in
51
+ * the tree, those central rules never match, and the chips silently fall back to the
52
+ * library's neutral defaults — which is a regression, not a theme.
53
+ */
54
+ chipClasses?: string;
13
55
  };
14
56
  export type ChatSuggestionsEventProperties = {
15
57
  onselect?: (value: string, index: number) => void;