@juspay/svelte-ui-components 4.33.3 → 4.34.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.
Files changed (46) hide show
  1. package/dist/Accordion/Accordion.svelte +26 -2
  2. package/dist/Accordion/properties.d.ts +7 -0
  3. package/dist/ArmedButton/ArmedButton.svelte +147 -0
  4. package/dist/ArmedButton/ArmedButton.svelte.d.ts +4 -0
  5. package/dist/ArmedButton/properties.d.ts +24 -0
  6. package/dist/ArmedButton/properties.js +1 -0
  7. package/dist/Banner/Banner.svelte +13 -8
  8. package/dist/Banner/properties.d.ts +8 -1
  9. package/dist/Button/Button.svelte +26 -0
  10. package/dist/Button/properties.d.ts +17 -0
  11. package/dist/ChatComposer/ChatComposer.svelte +314 -1
  12. package/dist/ChatComposer/properties.d.ts +42 -0
  13. package/dist/ChatMessageList/ChatMessageList.svelte +22 -2
  14. package/dist/ChatMessageList/ChatMessageList.svelte.d.ts +1 -1
  15. package/dist/CommandMenu/CommandMenu.svelte +53 -20
  16. package/dist/CommandMenu/properties.d.ts +24 -0
  17. package/dist/DiffViewer/DiffViewer.svelte +210 -0
  18. package/dist/DiffViewer/DiffViewer.svelte.d.ts +4 -0
  19. package/dist/DiffViewer/properties.d.ts +25 -0
  20. package/dist/DiffViewer/properties.js +1 -0
  21. package/dist/EmptyState/EmptyState.svelte +22 -3
  22. package/dist/EmptyState/properties.d.ts +9 -0
  23. package/dist/HITL/HITL.svelte +318 -49
  24. package/dist/HITL/properties.d.ts +113 -11
  25. package/dist/Input/Input.svelte +45 -1
  26. package/dist/Input/properties.d.ts +15 -0
  27. package/dist/KeyValue/KeyValue.svelte +16 -3
  28. package/dist/KeyValue/properties.d.ts +2 -0
  29. package/dist/Progress/Progress.svelte +96 -41
  30. package/dist/Progress/properties.d.ts +19 -0
  31. package/dist/Snippet/Snippet.svelte +45 -1
  32. package/dist/Snippet/properties.d.ts +8 -0
  33. package/dist/Tabs/Tabs.svelte +75 -16
  34. package/dist/Tabs/properties.d.ts +21 -0
  35. package/dist/ThinkingIndicator/ThinkingIndicator.svelte.d.ts +1 -1
  36. package/dist/Toolbar/Toolbar.svelte +81 -3
  37. package/dist/Toolbar/properties.d.ts +37 -0
  38. package/dist/WindowedList/WindowedList.svelte +129 -0
  39. package/dist/WindowedList/WindowedList.svelte.d.ts +25 -0
  40. package/dist/WindowedList/properties.d.ts +38 -0
  41. package/dist/WindowedList/properties.js +1 -0
  42. package/dist/index.d.ts +6 -0
  43. package/dist/index.js +3 -0
  44. package/dist/styles/theme-dark.css +42 -0
  45. package/dist-wc/index.js +2637 -1642
  46. package/package.json +2 -1
@@ -10,7 +10,8 @@
10
10
  classes,
11
11
  testId,
12
12
  disabled = false,
13
- panelId
13
+ panelId,
14
+ lazy = false
14
15
  }: AccordionProperties = $props();
15
16
 
16
17
  /* The trigger and the panel are siblings, not ancestor/descendant, so nothing in the
@@ -25,6 +26,19 @@
25
26
  control's id stays stable for assistive technology and anything else referencing it. */
26
27
  const triggerId = `accordion-trigger-${uid}`;
27
28
 
29
+ /* `lazy` mounts the panel's children the first time it opens and then keeps
30
+ them, so closing still animates real content and whatever state it holds
31
+ survives a close. The latch is a plain variable, not $state: it only ever
32
+ goes false -> true, and only while this derived recomputes because `expand`
33
+ or `lazy` changed, so it adds no dependency of its own. */
34
+ let hasOpened = false;
35
+ const mounted = $derived.by(() => {
36
+ if (!lazy || expand) {
37
+ hasOpened = true;
38
+ }
39
+ return hasOpened;
40
+ });
41
+
28
42
  function handleTriggerClick(): void {
29
43
  if (disabled) {
30
44
  return;
@@ -62,11 +76,21 @@
62
76
  aria-labelledby={trigger ? triggerId : null}
63
77
  class="accordion {classes ?? ''}"
64
78
  class:expanded={expand}
79
+ inert={lazy && !expand}
65
80
  data-pw={typeof testId === 'string' ? testId : null}
66
81
  testID={typeof testId === 'string' ? testId : null}
67
82
  >
83
+ <!-- `inert` while collapsed, lazy only: a lazy panel's content stays mounted
84
+ (not torn down) between opens, so without this its links/buttons would
85
+ stay in the tab order and the accessibility tree while clipped to 0px
86
+ and invisible. A non-lazy panel never reaches this state: `mounted` is
87
+ already unconditionally true for it, matching pre-`lazy` behaviour, so
88
+ marking it inert too would be a new, non-opt-in default-path change.
89
+ The children are chosen by a conditional snippet, not an {#if}: an {#if} would be a
90
+ new block around them, so a local transition inside would stop following the outer
91
+ block that adds or removes the Accordion. This stays the bare render tag it was. -->
68
92
  <div class="accordion-content">
69
- {@render children?.()}
93
+ {@render (mounted ? children : null)?.()}
70
94
  </div>
71
95
  </div>
72
96
 
@@ -24,6 +24,13 @@ export type OptionalAccordionProperties = {
24
24
  * something else needs to reference the panel by a known id.
25
25
  */
26
26
  panelId?: string;
27
+ /**
28
+ * Mounts `children` only once the panel is first expanded, then keeps them
29
+ * mounted. For long lists of mostly-closed panels, where rendering every
30
+ * closed body up front is the cost. Off by default: closed content is
31
+ * rendered (though `inert`) exactly as before.
32
+ */
33
+ lazy?: boolean;
27
34
  };
28
35
  export type AccordionEventProperties = {
29
36
  ontoggle?: (expanded: boolean) => void;
@@ -0,0 +1,147 @@
1
+ <script lang="ts">
2
+ import Button from '../Button/Button.svelte';
3
+ import { onDestroy, type Snippet } from 'svelte';
4
+ import type { ArmedButtonProperties } from './properties';
5
+
6
+ // Arm-then-confirm for a destructive action: first click arms, second
7
+ // within the window commits, and the arm lapses on its own. No component
8
+ // here models a two-stage confirmation any other way -- a consumer that
9
+ // needs one reaches for this instead of hand-rolling a boolean and a timer,
10
+ // which is exactly how a shared boolean across several rows previously left
11
+ // one control armed forever after a single mis-click.
12
+ //
13
+ // STYLING IS BY CUSTOM PROPERTY, NOT BY CLASS. `classes` lands on this
14
+ // component's own root, same as every other component here; the button
15
+ // underneath is a separate scope a consumer's own selector cannot reach
16
+ // directly, so every one of its properties is bridged through an
17
+ // --armed-btn-* hook instead.
18
+ //
19
+ // The contract that must survive any future change here: disarm happens
20
+ // BEFORE `onconfirm` is awaited, not after. A version that awaits first is
21
+ // a different component with different behaviour under a slow confirm --
22
+ // it would stay visually "armed" for the duration of the call, which reads
23
+ // as still-awaiting-a-second-click rather than as committed.
24
+
25
+ let {
26
+ onconfirm,
27
+ label,
28
+ confirmLabel = 'sure?',
29
+ armedMs = 3000,
30
+ disabled = false,
31
+ title,
32
+ icon,
33
+ classes
34
+ }: ArmedButtonProperties = $props();
35
+
36
+ let armed = $state(false);
37
+ let timer: ReturnType<typeof setTimeout> | null = null;
38
+
39
+ function disarm(): void {
40
+ armed = false;
41
+ if (timer !== null) {
42
+ clearTimeout(timer);
43
+ }
44
+ timer = null;
45
+ }
46
+
47
+ // Teardown matters here: an armed row that unmounts -- the list refreshes,
48
+ // the session ends -- must not leave a timer holding a reference to it.
49
+ onDestroy(() => {
50
+ if (timer !== null) {
51
+ clearTimeout(timer);
52
+ }
53
+ });
54
+
55
+ async function click(): Promise<void> {
56
+ if (timer !== null) {
57
+ clearTimeout(timer);
58
+ }
59
+ if (!armed) {
60
+ armed = true;
61
+ timer = setTimeout(disarm, armedMs);
62
+ return;
63
+ }
64
+ disarm();
65
+ await onconfirm();
66
+ }
67
+
68
+ // Button's `text`/`icon` are plain optionals, not nullable -- omitting the
69
+ // key entirely (via spread) is how a Svelte prop stays genuinely unset,
70
+ // rather than assigning it a value that means "unset".
71
+ const labelProps = $derived.by((): { text?: string; icon?: Snippet } => {
72
+ if (armed) {
73
+ return { text: confirmLabel };
74
+ }
75
+ if (typeof icon === 'function') {
76
+ return { icon };
77
+ }
78
+ return { text: label };
79
+ });
80
+ </script>
81
+
82
+ <span
83
+ class="armed-button {classes ?? ''}"
84
+ data-armed={armed}
85
+ role="presentation"
86
+ onfocusout={disarm}
87
+ >
88
+ <Button
89
+ {...labelProps}
90
+ {disabled}
91
+ title={title ?? (armed ? `click again to ${label.toLowerCase()}` : label)}
92
+ ariaLabel={armed ? `confirm: ${label}` : label}
93
+ onclick={click}
94
+ />
95
+ </span>
96
+
97
+ <style>
98
+ /* Every default below is a plain, generic small-button look, independent
99
+ of any consumer's own tokens -- exactly the two-layer fallback this
100
+ library uses throughout: override via --armed-btn-* if a consumer sets
101
+ it, sane default otherwise. */
102
+ .armed-button {
103
+ --button-color: var(--armed-btn-bg, #f0f0f0);
104
+ --button-hover-color: var(--armed-btn-hover-bg, var(--armed-btn-bg, #f0f0f0));
105
+ --button-text-color: var(--armed-btn-color, #4d4d4d);
106
+ --button-hover-text-color: var(--armed-btn-hover-color, #1a1a1a);
107
+ --button-border: var(--armed-btn-border, 1px solid #d0d0d0);
108
+ --button-border-radius: var(--armed-btn-radius, 7px);
109
+ --button-height: var(--armed-btn-height, 26px);
110
+ --button-width: var(--armed-btn-width, auto);
111
+ --button-padding: var(--armed-btn-padding, 0 8px);
112
+ --button-content-gap: var(--armed-btn-gap, 4px);
113
+ --button-font-size: var(--armed-btn-font-size, 12px);
114
+ --button-font-weight: var(--armed-btn-font-weight, 400);
115
+ --button-font-family: var(--armed-btn-font-family, inherit);
116
+ --button-justify-content: center;
117
+ --button-transition:
118
+ background var(--armed-btn-duration, 150ms) ease, color var(--armed-btn-duration, 150ms) ease;
119
+
120
+ --disabled-opacity: var(--armed-btn-disabled-opacity, 0.5);
121
+ --disabled-cursor: not-allowed;
122
+
123
+ display: var(--armed-btn-display, inline-flex);
124
+ margin-left: var(--armed-btn-margin-left, 0);
125
+ flex-shrink: var(--armed-btn-flex-shrink, 1);
126
+ font-variant-ligatures: var(--armed-btn-font-ligatures, normal);
127
+ }
128
+
129
+ /* Armed has its own geometry as well as its own colour: an icon-only
130
+ control is a square at rest and has to grow to fit "sure?" once armed. */
131
+ .armed-button[data-armed='true'] {
132
+ --button-color: var(--armed-btn-armed-bg, #dc2626);
133
+ --button-hover-color: var(--armed-btn-armed-bg, #dc2626);
134
+ --button-text-color: var(--armed-btn-armed-color, #ffffff);
135
+ --button-hover-text-color: var(--armed-btn-armed-color, #ffffff);
136
+ --button-border: 1px solid var(--armed-btn-armed-border, #dc2626);
137
+ --button-width: var(--armed-btn-armed-width, auto);
138
+ --button-padding: var(--armed-btn-armed-padding, 0 8px);
139
+ --button-font-weight: var(--armed-btn-armed-weight, 600);
140
+ }
141
+
142
+ @media (prefers-reduced-motion: reduce) {
143
+ .armed-button {
144
+ --button-transition: none;
145
+ }
146
+ }
147
+ </style>
@@ -0,0 +1,4 @@
1
+ import type { ArmedButtonProperties } from './properties';
2
+ declare const ArmedButton: import("svelte").Component<ArmedButtonProperties, {}, "">;
3
+ type ArmedButton = ReturnType<typeof ArmedButton>;
4
+ export default ArmedButton;
@@ -0,0 +1,24 @@
1
+ import type { Snippet } from 'svelte';
2
+ export type ArmedButtonProperties = {
3
+ /**
4
+ * Called on the SECOND click, after the button has disarmed. Disarming
5
+ * happens before this is awaited, not after -- a slow confirm must not
6
+ * hold the button in its armed, mid-confirmation-looking state.
7
+ */
8
+ onconfirm: () => void | Promise<void>;
9
+ /** Resting label. Replaced by `confirmLabel` while armed. */
10
+ label: string;
11
+ /** Shown in place of `label` while armed. Short -- it replaces the label, not beside it. */
12
+ confirmLabel?: string;
13
+ /** How long the armed state lasts before disarming itself. */
14
+ armedMs?: number;
15
+ disabled?: boolean;
16
+ /**
17
+ * Native title attribute. Defaults to `label` at rest and "click again to
18
+ * <label, lowercased>" while armed.
19
+ */
20
+ title?: string;
21
+ /** Replaces the resting label with an icon; `confirmLabel` still shows as text once armed. */
22
+ icon?: Snippet;
23
+ classes?: string;
24
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -10,7 +10,8 @@
10
10
  import closeSvg from '../assets/close.svg?raw';
11
11
 
12
12
  let {
13
- text,
13
+ text = '',
14
+ children,
14
15
  icon,
15
16
  linkText,
16
17
  dismissible = false,
@@ -73,12 +74,16 @@
73
74
  {@render title()}
74
75
  </div>
75
76
  {/if}
76
- <div class="banner-text">
77
- {text}
78
- {#if typeof linkText === 'string' && linkText.length > 0}
79
- <span class="banner-link-text">{linkText}</span>
80
- {/if}
81
- </div>
77
+ {#if typeof children === 'function'}
78
+ {@render children()}
79
+ {:else}
80
+ <div class="banner-text">
81
+ {text}
82
+ {#if typeof linkText === 'string' && linkText.length > 0}
83
+ <span class="banner-link-text">{linkText}</span>
84
+ {/if}
85
+ </div>
86
+ {/if}
82
87
  </div>
83
88
  {#if typeof rightContent === 'function'}
84
89
  <div class="banner-right">
@@ -169,7 +174,7 @@
169
174
  .banner-right {
170
175
  display: flex;
171
176
  align-items: center;
172
- flex-shrink: 0;
177
+ flex-shrink: var(--banner-right-flex-shrink, 0);
173
178
  }
174
179
 
175
180
  .banner-dismiss {
@@ -1,9 +1,16 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  export type BannerProperties = MandatoryBannerProperties & OptionalBannerProperties & BannerEventProperties;
3
3
  export type MandatoryBannerProperties = {
4
- text: string;
4
+ /** The message as plain text (with optional `linkText`). Defaults to ''. */
5
+ text?: string;
5
6
  };
6
7
  export type OptionalBannerProperties = {
8
+ /**
9
+ * The message as markup -- a sentence with a link, or a sentence and a
10
+ * Retry button -- rendered as the banner body in place of `text` and
11
+ * `linkText`. `title` still renders above it.
12
+ */
13
+ children?: Snippet;
7
14
  icon?: Snippet;
8
15
  title?: Snippet;
9
16
  linkText?: string;
@@ -25,9 +25,12 @@
25
25
  ariaControls,
26
26
  ariaHaspopup,
27
27
  ariaSelected,
28
+ ariaPressed,
28
29
  ariaBusy,
29
30
  role,
30
31
  title,
32
+ statusText,
33
+ statusTestId,
31
34
  onclick,
32
35
  onkeydown = () => {},
33
36
  onkeyup = () => {},
@@ -95,6 +98,7 @@
95
98
  aria-controls={ariaControls ?? null}
96
99
  aria-haspopup={ariaHaspopup ?? null}
97
100
  aria-selected={ariaSelected ?? null}
101
+ aria-pressed={ariaPressed ?? null}
98
102
  aria-busy={isBusy || ariaBusy || null}
99
103
  title={title ?? null}
100
104
  type={href ? null : type}
@@ -123,6 +127,11 @@
123
127
  {@render children()}
124
128
  {/if}
125
129
  </svelte:element>
130
+ {#if typeof statusText === 'string'}
131
+ <div class="button-status" role="status" aria-live="polite" data-pw={statusTestId ?? null}>
132
+ {statusText}
133
+ </div>
134
+ {/if}
126
135
  </div>
127
136
 
128
137
  <style>
@@ -131,6 +140,23 @@
131
140
  width: var(--button-width, fit-content);
132
141
  }
133
142
 
143
+ /* The statusText region: out of flow at 1x1px inside the positioned
144
+ container, so it never moves the button or its row (the visually-hidden
145
+ recipe ChatComposer and Label use). Not named .sr-only: `classes` lands on
146
+ this container, so a consumer's own `sr-only` class would match a rule of
147
+ that name here and hide the whole button. */
148
+ .button-status {
149
+ position: absolute;
150
+ width: 1px;
151
+ height: 1px;
152
+ padding: 0;
153
+ overflow: hidden;
154
+ clip: rect(0, 0, 0, 0);
155
+ clip-path: inset(50%);
156
+ white-space: nowrap;
157
+ border-width: 0;
158
+ }
159
+
134
160
  /* ---- Variant defaults (set the internal --_btn-* layer; explicit --button-* always wins) ---- */
135
161
  .variant-primary {
136
162
  --_btn-color: #3a4550;
@@ -64,6 +64,12 @@ export type OptionalButtonProperties = {
64
64
  */
65
65
  ariaHaspopup?: 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog' | boolean;
66
66
  ariaSelected?: boolean;
67
+ /**
68
+ * Native `aria-pressed`, for a toggle button (a chip that stays on once
69
+ * picked). `aria-selected` only means something on option/tab/row roles, so a
70
+ * plain button that toggles wants this instead.
71
+ */
72
+ ariaPressed?: boolean;
67
73
  /**
68
74
  * Native `aria-busy`, for a control that stays usable while related data loads.
69
75
  * Deliberately separate from `loading`, which also renders the spinner and
@@ -78,6 +84,17 @@ export type OptionalButtonProperties = {
78
84
  * affordance; an icon-only button generally wants both.
79
85
  */
80
86
  title?: string;
87
+ /**
88
+ * Opt-in sr-only status region (`role="status" aria-live="polite"`) rendered
89
+ * beside the button, never around it -- e.g. "Copied" after a copy button's
90
+ * click, announced without re-announcing the button itself. Rendered only
91
+ * when set (an empty string still renders an emptied region, which is how a
92
+ * caller keeps it mounted before the first announcement, as a live region
93
+ * must be). The sentence is the caller's; nothing is hardcoded here.
94
+ */
95
+ statusText?: string;
96
+ /** Test id on the status region (see `statusText`). */
97
+ statusTestId?: string;
81
98
  disabled?: boolean;
82
99
  classes?: string;
83
100
  /**