@juspay/svelte-ui-components 4.35.1 → 4.37.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.
@@ -21,6 +21,7 @@
21
21
  let {
22
22
  value = $bindable(''),
23
23
  layout = 'row',
24
+ variant = 'default',
24
25
  placeholder = '',
25
26
  disabled = false,
26
27
  textDisabled = null,
@@ -60,6 +61,7 @@
60
61
  actionLabel = 'Voice conversation',
61
62
  actionText,
62
63
  leading,
64
+ trailing,
63
65
  statusText,
64
66
  statusTestId,
65
67
  slashCommands = null,
@@ -71,6 +73,7 @@
71
73
  slashItem,
72
74
  onsubmit,
73
75
  oninput,
76
+ oninputfocus,
74
77
  onkeydown,
75
78
  onpaste,
76
79
  onstop,
@@ -92,6 +95,11 @@
92
95
  }: ChatComposerProperties = $props();
93
96
 
94
97
  let fileInput: HTMLInputElement | null = $state(null);
98
+ let inputElement: HTMLTextAreaElement | null = $state(null);
99
+
100
+ export const focus = (options?: FocusOptions): void => {
101
+ inputElement?.focus(options);
102
+ };
95
103
 
96
104
  let showVoice = $derived(typeof onvoice === 'function');
97
105
  let showAttach = $derived(typeof onattach === 'function' || typeof onattachclick === 'function');
@@ -391,6 +399,7 @@
391
399
  {#snippet textInput()}
392
400
  <textarea
393
401
  class="input"
402
+ bind:this={inputElement}
394
403
  data-pw={inputTestId ?? null}
395
404
  bind:value
396
405
  {placeholder}
@@ -404,6 +413,7 @@
404
413
  aria-activedescendant={slashActiveId}
405
414
  maxlength={maxLength > 0 ? maxLength : null}
406
415
  oninput={handleInput}
416
+ onfocus={(event) => oninputfocus?.(event)}
407
417
  onkeydown={handleKeydown}
408
418
  onpaste={(event) => onpaste?.(event)}
409
419
  use:autoGrow={value}
@@ -480,11 +490,15 @@
480
490
  </Button>
481
491
  </div>
482
492
  {/if}
493
+ {#if typeof trailing === 'function'}
494
+ {@render trailing()}
495
+ {/if}
483
496
  {/snippet}
484
497
 
485
498
  <div
486
499
  class="chat-composer {classes ?? ''}"
487
500
  class:disabled
501
+ class:pill={variant === 'pill'}
488
502
  data-has-slash={slashCommands !== null ? '' : null}
489
503
  data-pw={typeof testId === 'string' ? testId : null}
490
504
  testID={typeof testId === 'string' ? testId : null}
@@ -611,6 +625,10 @@
611
625
  opacity: var(--chat-composer-disabled-opacity, 0.6);
612
626
  }
613
627
 
628
+ .chat-composer.pill {
629
+ border-radius: var(--chat-composer-pill-border-radius, 999px);
630
+ }
631
+
614
632
  /* .input below sets outline: none with nothing standing in for it, so the
615
633
  visible replacement lives here on the surrounding surface instead --
616
634
  :focus-within lights up whenever the textarea inside is focused, the same
@@ -1,4 +1,6 @@
1
1
  import type { ChatComposerProperties } from './properties';
2
- declare const ChatComposer: import("svelte").Component<ChatComposerProperties, {}, "value" | "attachments">;
2
+ declare const ChatComposer: import("svelte").Component<ChatComposerProperties, {
3
+ focus: (options?: FocusOptions) => void;
4
+ }, "value" | "attachments">;
3
5
  type ChatComposer = ReturnType<typeof ChatComposer>;
4
6
  export default ChatComposer;
@@ -5,6 +5,7 @@ export type OptionalChatComposerProperties = {
5
5
  value?: string;
6
6
  /** Text above a separate control row when stacked; row preserves the default. */
7
7
  layout?: 'row' | 'stacked';
8
+ variant?: 'default' | 'pill';
8
9
  placeholder?: string;
9
10
  disabled?: boolean;
10
11
  /**
@@ -85,6 +86,8 @@ export type OptionalChatComposerProperties = {
85
86
  /** Optional visible text beside the idle action icon; omitted keeps icon-only markup. */
86
87
  actionText?: string;
87
88
  leading?: Snippet;
89
+ /** Additional controls after the send/stop control. */
90
+ trailing?: Snippet;
88
91
  /**
89
92
  * Opt-in sr-only status region (`role="status" aria-live="polite"`) for
90
93
  * narrating composer state changes -- e.g. "Recording. Press Escape to stop
@@ -145,6 +148,8 @@ export type ChatComposerEventProperties = {
145
148
  */
146
149
  onsubmit?: (value: string, attachments: File[]) => boolean | void | Promise<boolean | void>;
147
150
  oninput?: (value: string, event: Event) => void;
151
+ /** Textarea focus, named separately from the native HTMLElement onfocus accessor. */
152
+ oninputfocus?: (event: FocusEvent) => void;
148
153
  onkeydown?: (event: KeyboardEvent) => void;
149
154
  onpaste?: (event: ClipboardEvent) => void;
150
155
  onstop?: () => void;
@@ -2,6 +2,8 @@
2
2
  import Button from '../Button/Button.svelte';
3
3
  import Img from '../Img/Img.svelte';
4
4
  import closeSvg from '../assets/close.svg?raw';
5
+ import fullscreenSvg from '../assets/fullscreen.svg?raw';
6
+ import exitFullscreenSvg from '../assets/exit-fullscreen.svg?raw';
5
7
  import type { ChatHeaderProperties } from './properties';
6
8
 
7
9
  let {
@@ -11,6 +13,11 @@
11
13
  imageAlt = '',
12
14
  avatar,
13
15
  actions,
16
+ expanded = $bindable(false),
17
+ showExpand,
18
+ expandLabel = 'Expand',
19
+ collapseLabel = 'Collapse',
20
+ onexpandchange,
14
21
  closeIcon,
15
22
  closeLabel = 'Close',
16
23
  showClose,
@@ -21,6 +28,12 @@
21
28
  }: ChatHeaderProperties = $props();
22
29
 
23
30
  let displayClose = $derived(showClose ?? typeof onclose === 'function');
31
+ let displayExpand = $derived(showExpand ?? typeof onexpandchange === 'function');
32
+
33
+ const toggleExpanded = (): void => {
34
+ expanded = !expanded;
35
+ onexpandchange?.(expanded);
36
+ };
24
37
  </script>
25
38
 
26
39
  <header
@@ -53,6 +66,18 @@
53
66
  {#if typeof actions === 'function'}
54
67
  {@render actions()}
55
68
  {/if}
69
+ {#if displayExpand}
70
+ <div class="expand">
71
+ <Button
72
+ variant="ghost"
73
+ onclick={toggleExpanded}
74
+ ariaLabel={expanded ? collapseLabel : expandLabel}
75
+ >
76
+ <!-- eslint-disable-next-line svelte/no-at-html-tags -->
77
+ {@html expanded ? exitFullscreenSvg : fullscreenSvg}
78
+ </Button>
79
+ </div>
80
+ {/if}
56
81
  {#if displayClose}
57
82
  <div class="close">
58
83
  <Button onclick={() => onclose?.()} ariaLabel={closeLabel}>
@@ -166,4 +191,21 @@
166
191
  height: 100%;
167
192
  width: 100%;
168
193
  }
194
+
195
+ .expand {
196
+ --button-width: var(--chat-header-expand-size, var(--chat-header-close-size, 36px));
197
+ --button-height: var(--chat-header-expand-size, var(--chat-header-close-size, 36px));
198
+ --button-padding: var(--chat-header-expand-padding, var(--chat-header-close-padding, 8px));
199
+ --button-border-radius: var(--chat-header-expand-border-radius, 50%);
200
+ --button-color: var(--chat-header-expand-background-color, transparent);
201
+ --button-text-color: var(--chat-header-expand-color, #52525b);
202
+ --button-hover-text-color: var(--chat-header-expand-color, #52525b);
203
+ --button-content-gap: 0px;
204
+ --button-hover-color: var(--chat-header-expand-hover-background-color, #f4f4f5);
205
+ }
206
+
207
+ .expand :global(svg) {
208
+ height: 100%;
209
+ width: 100%;
210
+ }
169
211
  </style>
@@ -1,4 +1,4 @@
1
1
  import type { ChatHeaderProperties } from './properties';
2
- declare const ChatHeader: import("svelte").Component<ChatHeaderProperties, {}, "">;
2
+ declare const ChatHeader: import("svelte").Component<ChatHeaderProperties, {}, "expanded">;
3
3
  type ChatHeader = ReturnType<typeof ChatHeader>;
4
4
  export default ChatHeader;
@@ -7,6 +7,10 @@ export type OptionalChatHeaderProperties = {
7
7
  imageAlt?: string;
8
8
  avatar?: Snippet;
9
9
  actions?: Snippet;
10
+ expanded?: boolean;
11
+ showExpand?: boolean;
12
+ expandLabel?: string;
13
+ collapseLabel?: string;
10
14
  closeIcon?: Snippet;
11
15
  closeLabel?: string;
12
16
  showClose?: boolean;
@@ -16,4 +20,5 @@ export type OptionalChatHeaderProperties = {
16
20
  };
17
21
  export type ChatHeaderEventProperties = {
18
22
  onclose?: () => void;
23
+ onexpandchange?: (expanded: boolean) => void;
19
24
  };
@@ -0,0 +1,193 @@
1
+ <script lang="ts">
2
+ import Resizable from '../Resizable/Resizable.svelte';
3
+ import { registerDismissible } from '../_interaction/dismissal';
4
+ import type { ResizeSize } from '../Resizable/properties';
5
+ import type { DockPanelProperties, DockPanelSnapshot, DockPanelState } from './properties';
6
+
7
+ let {
8
+ state: panelState = $bindable('closed'),
9
+ width = $bindable(380),
10
+ minWidth = 300,
11
+ maxWidth = 600,
12
+ reservedWidth = $bindable(0),
13
+ label = 'Panel',
14
+ resizeLabel = 'Resize panel',
15
+ header,
16
+ body,
17
+ footer,
18
+ children,
19
+ testId,
20
+ classes,
21
+ onstatechange,
22
+ onwidthchange
23
+ }: DockPanelProperties = $props();
24
+
25
+ const lowerWidth = $derived(Number.isFinite(minWidth) ? Math.max(0, minWidth) : 300);
26
+ const upperWidth = $derived(
27
+ Number.isFinite(maxWidth) ? Math.max(lowerWidth, maxWidth) : Math.max(lowerWidth, 600)
28
+ );
29
+ const clampedWidth = $derived(
30
+ Math.min(upperWidth, Math.max(lowerWidth, Number.isFinite(width) ? width : 380))
31
+ );
32
+
33
+ const changeState = (nextState: DockPanelState): void => {
34
+ if (panelState !== nextState) {
35
+ panelState = nextState;
36
+ onstatechange?.(nextState);
37
+ }
38
+ };
39
+ const expand = (): void => changeState('expanded');
40
+ const collapse = (): void => changeState('docked');
41
+ const close = (): void => changeState('closed');
42
+
43
+ const handleResize = (size: ResizeSize): void => {
44
+ const nextWidth = Math.min(upperWidth, Math.max(lowerWidth, size.width));
45
+ if (width !== nextWidth) {
46
+ width = nextWidth;
47
+ onwidthchange?.(width);
48
+ }
49
+ };
50
+
51
+ const synchronizePanel = (node: HTMLElement, snapshot: DockPanelSnapshot) => {
52
+ let releaseDismissal: (() => void) | null = null;
53
+ const update = (nextSnapshot: DockPanelSnapshot): void => {
54
+ if (width !== nextSnapshot.width) {
55
+ width = nextSnapshot.width;
56
+ onwidthchange?.(width);
57
+ }
58
+ const nextReservation = nextSnapshot.state === 'docked' ? nextSnapshot.width : 0;
59
+ if (reservedWidth !== nextReservation) {
60
+ reservedWidth = nextReservation;
61
+ }
62
+ if (nextSnapshot.state === 'expanded' && releaseDismissal === null) {
63
+ releaseDismissal = registerDismissible({ element: () => node, onEscape: collapse });
64
+ } else if (nextSnapshot.state !== 'expanded' && releaseDismissal !== null) {
65
+ releaseDismissal();
66
+ releaseDismissal = null;
67
+ }
68
+ };
69
+ update(snapshot);
70
+ return { update, destroy: () => releaseDismissal?.() };
71
+ };
72
+ </script>
73
+
74
+ <div
75
+ class="dock-panel {classes ?? ''}"
76
+ class:closed={panelState === 'closed'}
77
+ class:expanded={panelState === 'expanded'}
78
+ data-state={panelState}
79
+ data-pw={typeof testId === 'string' ? testId : null}
80
+ testID={typeof testId === 'string' ? testId : null}
81
+ role="complementary"
82
+ aria-label={label}
83
+ inert={panelState === 'closed'}
84
+ use:synchronizePanel={{ state: panelState, width: clampedWidth }}
85
+ style:width={panelState === 'expanded' ? null : `${clampedWidth}px`}
86
+ >
87
+ <Resizable
88
+ width={panelState === 'docked' ? clampedWidth : null}
89
+ minWidth={lowerWidth}
90
+ maxWidth={upperWidth}
91
+ handles={panelState === 'docked' ? ['left'] : []}
92
+ handleLabel={resizeLabel}
93
+ onresize={handleResize}
94
+ classes="dock-panel-frame"
95
+ >
96
+ {#if typeof header === 'function'}
97
+ <div class="header">{@render header({ state: panelState, expand, collapse, close })}</div>
98
+ {/if}
99
+ <div class="body">
100
+ {#if typeof body === 'function'}
101
+ {@render body()}
102
+ {:else if typeof children === 'function'}
103
+ {@render children()}
104
+ {/if}
105
+ </div>
106
+ {#if typeof footer === 'function'}
107
+ <div class="footer">{@render footer()}</div>
108
+ {/if}
109
+ </Resizable>
110
+ </div>
111
+
112
+ <style>
113
+ .dock-panel {
114
+ box-sizing: border-box;
115
+ position: var(--dock-panel-position, fixed);
116
+ top: var(--dock-panel-top, 0px);
117
+ right: var(--dock-panel-right, 0px);
118
+ bottom: var(--dock-panel-bottom, 0px);
119
+ display: flex;
120
+ flex-direction: column;
121
+ min-width: 0;
122
+ z-index: var(--dock-panel-z-index, 30);
123
+ background: var(--dock-panel-background, #ffffff);
124
+ color: var(--dock-panel-color, #18181b);
125
+ border-radius: var(--dock-panel-border-radius, 0px);
126
+ box-shadow: var(--dock-panel-box-shadow, -4px 0 16px #00000012);
127
+ opacity: 1;
128
+ transform: translateX(0);
129
+ transition:
130
+ opacity
131
+ var(--dock-panel-transition-duration, var(--duration-base, var(--motion-duration, 200ms)))
132
+ var(--dock-panel-transition-easing, var(--ease-smooth-out, var(--motion-easing, ease-out))),
133
+ transform
134
+ var(--dock-panel-transition-duration, var(--duration-base, var(--motion-duration, 200ms)))
135
+ var(--dock-panel-transition-easing, var(--ease-smooth-out, var(--motion-easing, ease-out))),
136
+ display
137
+ var(--dock-panel-transition-duration, var(--duration-base, var(--motion-duration, 200ms)))
138
+ allow-discrete;
139
+ }
140
+
141
+ .dock-panel.closed {
142
+ display: none;
143
+ opacity: 0;
144
+ transform: translateX(var(--dock-panel-enter-distance, var(--distance-base, 8px)));
145
+ }
146
+
147
+ .dock-panel.expanded {
148
+ left: var(--dock-panel-left, 0px);
149
+ width: calc(100% - var(--dock-panel-left, 0px) - var(--dock-panel-right, 0px));
150
+ }
151
+
152
+ .dock-panel :global(.dock-panel-frame) {
153
+ display: flex;
154
+ flex-direction: column;
155
+ flex: 1;
156
+ min-height: 0;
157
+ min-width: 0;
158
+ border: var(--dock-panel-border, 1px solid #e4e4e7);
159
+ --resizable-transition: none;
160
+ }
161
+
162
+ .header {
163
+ flex-shrink: 0;
164
+ padding: var(--dock-panel-header-padding, 0px);
165
+ border-bottom: var(--dock-panel-header-border, none);
166
+ }
167
+
168
+ .body {
169
+ flex: 1;
170
+ min-height: 0;
171
+ overflow: auto;
172
+ padding: var(--dock-panel-body-padding, 0px);
173
+ }
174
+
175
+ .footer {
176
+ flex-shrink: 0;
177
+ padding: var(--dock-panel-footer-padding, 0px);
178
+ border-top: var(--dock-panel-footer-border, none);
179
+ }
180
+
181
+ @starting-style {
182
+ .dock-panel:not(.closed) {
183
+ opacity: 0;
184
+ transform: translateX(var(--dock-panel-enter-distance, var(--distance-base, 8px)));
185
+ }
186
+ }
187
+
188
+ @media (prefers-reduced-motion: reduce) {
189
+ .dock-panel {
190
+ transition: none;
191
+ }
192
+ }
193
+ </style>
@@ -0,0 +1,4 @@
1
+ import type { DockPanelProperties } from './properties';
2
+ declare const DockPanel: import("svelte").Component<DockPanelProperties, {}, "state" | "width" | "reservedWidth">;
3
+ type DockPanel = ReturnType<typeof DockPanel>;
4
+ export default DockPanel;
@@ -0,0 +1,30 @@
1
+ import type { Snippet } from 'svelte';
2
+ export type DockPanelState = 'closed' | 'docked' | 'expanded';
3
+ export type DockPanelControls = {
4
+ state: DockPanelState;
5
+ expand: () => void;
6
+ collapse: () => void;
7
+ close: () => void;
8
+ };
9
+ export type DockPanelSnapshot = {
10
+ state: DockPanelState;
11
+ width: number;
12
+ };
13
+ export type DockPanelProperties = {
14
+ state?: DockPanelState;
15
+ width?: number;
16
+ minWidth?: number;
17
+ maxWidth?: number;
18
+ /** Published layout reservation. Bind to observe; zero when closed or expanded. */
19
+ reservedWidth?: number;
20
+ label?: string;
21
+ resizeLabel?: string;
22
+ header?: Snippet<[DockPanelControls]>;
23
+ body?: Snippet;
24
+ footer?: Snippet;
25
+ children?: Snippet;
26
+ testId?: string;
27
+ classes?: string;
28
+ onstatechange?: (state: DockPanelState) => void;
29
+ onwidthchange?: (width: number) => void;
30
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -88,8 +88,13 @@
88
88
  event.preventDefault();
89
89
  const startW = clampWidth(measuredWidth());
90
90
  const startH = clampHeight(measuredHeight());
91
- width = startW;
92
- height = startH;
91
+ const direction = DIRS[edge];
92
+ if (direction.x !== 0) {
93
+ width = startW;
94
+ }
95
+ if (direction.y !== 0) {
96
+ height = startH;
97
+ }
93
98
  active = {
94
99
  edge,
95
100
  pointerId: event.pointerId,
@@ -137,18 +142,22 @@
137
142
  let handled = false;
138
143
 
139
144
  if (dir.x !== 0 && (event.key === 'ArrowRight' || event.key === 'ArrowLeft')) {
140
- nextWidth = clampWidth(nextWidth + (event.key === 'ArrowRight' ? step : -step));
145
+ nextWidth = clampWidth(nextWidth + dir.x * (event.key === 'ArrowRight' ? step : -step));
141
146
  handled = true;
142
147
  }
143
148
  if (dir.y !== 0 && (event.key === 'ArrowDown' || event.key === 'ArrowUp')) {
144
- nextHeight = clampHeight(nextHeight + (event.key === 'ArrowDown' ? step : -step));
149
+ nextHeight = clampHeight(nextHeight + dir.y * (event.key === 'ArrowDown' ? step : -step));
145
150
  handled = true;
146
151
  }
147
152
 
148
153
  if (handled) {
149
154
  event.preventDefault();
150
- width = nextWidth;
151
- height = nextHeight;
155
+ if (dir.x !== 0 && (event.key === 'ArrowRight' || event.key === 'ArrowLeft')) {
156
+ width = nextWidth;
157
+ }
158
+ if (dir.y !== 0 && (event.key === 'ArrowDown' || event.key === 'ArrowUp')) {
159
+ height = nextHeight;
160
+ }
152
161
  onresize?.({ width: nextWidth, height: nextHeight });
153
162
  }
154
163
  }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  export { default as ArmedButton } from './ArmedButton/ArmedButton.svelte';
2
2
  export { default as Modal } from './Modal/Modal.svelte';
3
+ export { default as DockPanel } from './DockPanel/DockPanel.svelte';
4
+ export type { DockPanelState, DockPanelControls, DockPanelProperties } from './DockPanel/properties';
3
5
  export { default as BrandLoader } from './BrandLoader/BrandLoader.svelte';
4
6
  export { default as Button } from './Button/Button.svelte';
5
7
  export { default as Input } from './Input/Input.svelte';
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export { default as ArmedButton } from './ArmedButton/ArmedButton.svelte';
2
2
  export { default as Modal } from './Modal/Modal.svelte';
3
+ export { default as DockPanel } from './DockPanel/DockPanel.svelte';
3
4
  export { default as BrandLoader } from './BrandLoader/BrandLoader.svelte';
4
5
  export { default as Button } from './Button/Button.svelte';
5
6
  export { default as Input } from './Input/Input.svelte';
@@ -167,6 +167,11 @@
167
167
  --chat-header-subtitle-color: #9ca3af;
168
168
  --chat-header-close-color: #9ca3af;
169
169
  --chat-header-close-hover-background-color: #252535;
170
+ --chat-header-expand-color: #9ca3af;
171
+ --chat-header-expand-hover-background-color: #252535;
172
+ --dock-panel-background: #17171f;
173
+ --dock-panel-color: #e5e7eb;
174
+ --dock-panel-border: 1px solid #30303d;
170
175
 
171
176
  /* ChatMessageList */
172
177
  --chat-message-list-jump-background-color: #1e1e2e;
@@ -17,7 +17,7 @@
17
17
  * stylesheet is a crutch for the migration window, not a permanent override.
18
18
  */
19
19
 
20
- /* components whose new default is `block` (88) */
20
+ /* components whose new default is `block` (89) */
21
21
  sui-accordion { --sui-accordion-display: inline; }
22
22
  sui-area-chart { --sui-area-chart-display: inline; }
23
23
  sui-aspect-ratio { --sui-aspect-ratio-display: inline; }
@@ -47,6 +47,7 @@ sui-color-picker { --sui-color-picker-display: inline; }
47
47
  sui-combobox { --sui-combobox-display: inline; }
48
48
  sui-command-menu { --sui-command-menu-display: inline; }
49
49
  sui-context-menu { --sui-context-menu-display: inline; }
50
+ sui-dock-panel { --sui-dock-panel-display: inline; }
50
51
  sui-draggable { --sui-draggable-display: inline; }
51
52
  sui-dual-axis-bar-chart { --sui-dual-axis-bar-chart-display: inline; }
52
53
  sui-empty-state { --sui-empty-state-display: inline; }
@@ -174,6 +174,11 @@
174
174
  "property": "--sui-delta-indicator-display",
175
175
  "default": "inline-block"
176
176
  },
177
+ {
178
+ "tag": "sui-dock-panel",
179
+ "property": "--sui-dock-panel-display",
180
+ "default": "block"
181
+ },
177
182
  {
178
183
  "tag": "sui-draggable",
179
184
  "property": "--sui-draggable-display",