@juspay/svelte-ui-components 2.123.0 → 2.125.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.
- package/dist/Button/Button.svelte +4 -1
- package/dist/Button/properties.d.ts +13 -0
- package/dist/Chat/Chat.svelte +158 -0
- package/dist/Chat/Chat.svelte.d.ts +4 -0
- package/dist/Chat/controller.svelte.d.ts +24 -0
- package/dist/Chat/controller.svelte.js +190 -0
- package/dist/Chat/properties.d.ts +51 -0
- package/dist/Chat/properties.js +1 -0
- package/dist/Chat/roles.d.ts +2 -0
- package/dist/Chat/roles.js +3 -0
- package/dist/Chat/types.d.ts +45 -0
- package/dist/Chat/types.js +1 -0
- package/dist/ChatBubble/ChatBubble.svelte +419 -0
- package/dist/ChatBubble/ChatBubble.svelte.d.ts +4 -0
- package/dist/ChatBubble/properties.d.ts +43 -0
- package/dist/ChatBubble/properties.js +1 -0
- package/dist/ChatComposer/ChatComposer.svelte +289 -0
- package/dist/ChatComposer/ChatComposer.svelte.d.ts +4 -0
- package/dist/ChatComposer/properties.d.ts +33 -0
- package/dist/ChatComposer/properties.js +1 -0
- package/dist/ChatHeader/ChatHeader.svelte +169 -0
- package/dist/ChatHeader/ChatHeader.svelte.d.ts +4 -0
- package/dist/ChatHeader/properties.d.ts +19 -0
- package/dist/ChatHeader/properties.js +1 -0
- package/dist/ChatMessage/ChatMessage.svelte +330 -0
- package/dist/ChatMessage/ChatMessage.svelte.d.ts +4 -0
- package/dist/ChatMessage/properties.d.ts +29 -0
- package/dist/ChatMessage/properties.js +1 -0
- package/dist/ChatMessageList/ChatMessageList.svelte +181 -0
- package/dist/ChatMessageList/ChatMessageList.svelte.d.ts +4 -0
- package/dist/ChatMessageList/properties.d.ts +22 -0
- package/dist/ChatMessageList/properties.js +1 -0
- package/dist/ChatSuggestions/ChatSuggestions.svelte +46 -0
- package/dist/ChatSuggestions/ChatSuggestions.svelte.d.ts +4 -0
- package/dist/ChatSuggestions/properties.d.ts +16 -0
- package/dist/ChatSuggestions/properties.js +1 -0
- package/dist/ChatToolStatus/ChatToolStatus.svelte +63 -0
- package/dist/ChatToolStatus/ChatToolStatus.svelte.d.ts +4 -0
- package/dist/ChatToolStatus/properties.d.ts +10 -0
- package/dist/ChatToolStatus/properties.js +1 -0
- package/dist/Input/Input.svelte +32 -8
- package/dist/Input/properties.d.ts +6 -1
- package/dist/LineChart/LineChart.svelte +36 -15
- package/dist/Menu/Menu.svelte +39 -3
- package/dist/Resizable/Resizable.svelte +304 -0
- package/dist/Resizable/Resizable.svelte.d.ts +4 -0
- package/dist/Resizable/properties.d.ts +27 -0
- package/dist/Resizable/properties.js +1 -0
- package/dist/assets/attach.svg +3 -0
- package/dist/assets/chat.svg +3 -0
- package/dist/assets/mic.svg +5 -0
- package/dist/assets/retry.svg +4 -0
- package/dist/assets/send.svg +4 -0
- package/dist/assets/stop.svg +3 -0
- package/dist/assets/thumb-down.svg +4 -0
- package/dist/assets/thumb-up.svg +4 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +11 -0
- package/package.json +1 -1
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Loader from '../Loader/Loader.svelte';
|
|
3
|
+
import type { ChatToolStatusProperties } from './properties';
|
|
4
|
+
|
|
5
|
+
let { label, icon, testId, classes }: ChatToolStatusProperties = $props();
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
<div
|
|
9
|
+
class="chat-tool-status {classes ?? ''}"
|
|
10
|
+
aria-live="polite"
|
|
11
|
+
data-pw={typeof testId === 'string' ? testId : null}
|
|
12
|
+
testID={typeof testId === 'string' ? testId : null}
|
|
13
|
+
>
|
|
14
|
+
<span class="indicator">
|
|
15
|
+
{#if typeof icon === 'function'}
|
|
16
|
+
{@render icon()}
|
|
17
|
+
{:else}
|
|
18
|
+
<Loader />
|
|
19
|
+
{/if}
|
|
20
|
+
</span>
|
|
21
|
+
<span class="label">{label}</span>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<style>
|
|
25
|
+
.chat-tool-status {
|
|
26
|
+
box-sizing: border-box;
|
|
27
|
+
display: inline-flex;
|
|
28
|
+
align-items: center;
|
|
29
|
+
gap: var(--chat-tool-status-gap, 8px);
|
|
30
|
+
width: fit-content;
|
|
31
|
+
padding: var(--chat-tool-status-padding, 8px 14px);
|
|
32
|
+
background: var(--chat-tool-status-background, #ffffff);
|
|
33
|
+
border: var(--chat-tool-status-border, 1px solid #e4e4e7);
|
|
34
|
+
border-radius: var(--chat-tool-status-border-radius, 999px);
|
|
35
|
+
box-shadow: var(--chat-tool-status-box-shadow, 0 6px 20px rgba(0, 0, 0, 0.08));
|
|
36
|
+
color: var(--chat-tool-status-color, #52525b);
|
|
37
|
+
font-size: var(--chat-tool-status-font-size, 0.85rem);
|
|
38
|
+
font-weight: var(--chat-tool-status-font-weight, 500);
|
|
39
|
+
max-width: var(--chat-tool-status-max-width, 100%);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.indicator {
|
|
43
|
+
display: inline-flex;
|
|
44
|
+
align-items: center;
|
|
45
|
+
flex-shrink: 0;
|
|
46
|
+
color: var(--chat-tool-status-indicator-color, currentColor);
|
|
47
|
+
--loader-foreground: var(--chat-tool-status-spinner-color, currentColor);
|
|
48
|
+
--loader-foreground-end: var(--chat-tool-status-spinner-color-end, transparent);
|
|
49
|
+
--loader-width: var(--chat-tool-status-spinner-size, 14px);
|
|
50
|
+
--loader-height: var(--chat-tool-status-spinner-size, 14px);
|
|
51
|
+
--loader-before-width: 7px;
|
|
52
|
+
--loader-before-height: 7px;
|
|
53
|
+
--loader-after-width: 11px;
|
|
54
|
+
--loader-after-height: 11px;
|
|
55
|
+
--loader-background: var(--chat-tool-status-background, #ffffff);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.label {
|
|
59
|
+
white-space: nowrap;
|
|
60
|
+
overflow: hidden;
|
|
61
|
+
text-overflow: ellipsis;
|
|
62
|
+
}
|
|
63
|
+
</style>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
export type ChatToolStatusProperties = OptionalChatToolStatusProperties & MandatoryChatToolStatusProperties;
|
|
3
|
+
export type MandatoryChatToolStatusProperties = {
|
|
4
|
+
label: string;
|
|
5
|
+
};
|
|
6
|
+
export type OptionalChatToolStatusProperties = {
|
|
7
|
+
icon?: Snippet;
|
|
8
|
+
testId?: string;
|
|
9
|
+
classes?: string;
|
|
10
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/Input/Input.svelte
CHANGED
|
@@ -115,6 +115,9 @@
|
|
|
115
115
|
const hasRightIcon = $derived(typeof rightIcon === 'function');
|
|
116
116
|
|
|
117
117
|
const charCount = $derived(value?.length ?? 0);
|
|
118
|
+
// Every numeric use below is either tel-only normalisation or the character
|
|
119
|
+
// counter; `null` means "no attribute", not "no ceiling on those paths".
|
|
120
|
+
const effectiveMaxLength = $derived(maxLength ?? 1000);
|
|
118
121
|
const effectiveResize = $derived(autoResize ? 'none' : resize);
|
|
119
122
|
|
|
120
123
|
// Grow the textarea to fit its content between minRows and maxRows.
|
|
@@ -130,8 +133,16 @@
|
|
|
130
133
|
const border = parseFloat(styles.borderTopWidth) + parseFloat(styles.borderBottomWidth);
|
|
131
134
|
const lower = minRows ?? rows ?? 2;
|
|
132
135
|
const minHeight = lower * lineHeight + verticalPadding + border;
|
|
133
|
-
const
|
|
136
|
+
const rowsCeiling =
|
|
134
137
|
maxRows != null ? maxRows * lineHeight + verticalPadding + border : Number.POSITIVE_INFINITY;
|
|
138
|
+
// --input-max-height is a ceiling too. Reading only maxRows left it at Infinity, so the
|
|
139
|
+
// inline height grew past the CSS clamp and overflowY was set to `hidden` — the box
|
|
140
|
+
// stopped at the right size but its overflow became unreachable instead of scrollable.
|
|
141
|
+
const styleCeiling = parseFloat(styles.maxHeight);
|
|
142
|
+
const maxHeight = Math.min(
|
|
143
|
+
rowsCeiling,
|
|
144
|
+
Number.isFinite(styleCeiling) ? styleCeiling : Number.POSITIVE_INFINITY
|
|
145
|
+
);
|
|
135
146
|
const nextHeight = Math.min(Math.max(el.scrollHeight, minHeight), maxHeight);
|
|
136
147
|
el.style.height = `${nextHeight}px`;
|
|
137
148
|
el.style.overflowY = el.scrollHeight > maxHeight ? 'auto' : 'hidden';
|
|
@@ -163,16 +174,16 @@
|
|
|
163
174
|
inputElement.value = value;
|
|
164
175
|
return;
|
|
165
176
|
}
|
|
166
|
-
if (numberLength >
|
|
177
|
+
if (numberLength > effectiveMaxLength) {
|
|
167
178
|
const existingInput = value;
|
|
168
|
-
if (existingInput.length ===
|
|
179
|
+
if (existingInput.length === effectiveMaxLength) {
|
|
169
180
|
inputElement.value = applyTextPresentation(value);
|
|
170
181
|
return;
|
|
171
182
|
}
|
|
172
183
|
/**
|
|
173
184
|
* choose last max length number of digits if length is bigger than max length passed in props
|
|
174
185
|
*/
|
|
175
|
-
currentValue = currentValue.substring(numberLength -
|
|
186
|
+
currentValue = currentValue.substring(numberLength - effectiveMaxLength);
|
|
176
187
|
}
|
|
177
188
|
currentValue = applyTextPresentation(currentValue);
|
|
178
189
|
inputElement.value = currentValue;
|
|
@@ -221,12 +232,12 @@
|
|
|
221
232
|
/**
|
|
222
233
|
* user pasted 10+ digit number , overrides all cases
|
|
223
234
|
*/
|
|
224
|
-
if (filteredNumber.length >
|
|
235
|
+
if (filteredNumber.length > effectiveMaxLength) {
|
|
225
236
|
/**
|
|
226
237
|
* choose last max length number of digits if length is bigger than max length passed in props
|
|
227
238
|
*/
|
|
228
239
|
const finalValue = applyTextPresentation(
|
|
229
|
-
filteredNumber.substring(filteredNumberLength -
|
|
240
|
+
filteredNumber.substring(filteredNumberLength - effectiveMaxLength)
|
|
230
241
|
);
|
|
231
242
|
// Adding reactivity
|
|
232
243
|
value = finalValue;
|
|
@@ -390,8 +401,8 @@
|
|
|
390
401
|
</div>
|
|
391
402
|
{/if}
|
|
392
403
|
{#if useTextArea && showCount && !actionInput}
|
|
393
|
-
<div class="input-char-count" class:at-limit={charCount >=
|
|
394
|
-
{charCount}/{
|
|
404
|
+
<div class="input-char-count" class:at-limit={charCount >= effectiveMaxLength}>
|
|
405
|
+
{charCount}/{effectiveMaxLength}
|
|
395
406
|
</div>
|
|
396
407
|
{/if}
|
|
397
408
|
</div>
|
|
@@ -401,6 +412,13 @@
|
|
|
401
412
|
input {
|
|
402
413
|
box-sizing: var(--input-box-sizing, border-box);
|
|
403
414
|
height: var(--input-height, fit-content);
|
|
415
|
+
|
|
416
|
+
/* Both default to the CSS initial value, so a consumer that sets neither is
|
|
417
|
+
byte-identical to before. A textarea that grows with its content needs a
|
|
418
|
+
ceiling before it can scroll, and one used as a paste target needs a floor;
|
|
419
|
+
neither was reachable through the --input-* surface. */
|
|
420
|
+
min-height: var(--input-min-height, auto);
|
|
421
|
+
max-height: var(--input-max-height, none);
|
|
404
422
|
background-color: var(--input-background, white);
|
|
405
423
|
font-size: var(--input-font-size, 16px) !important;
|
|
406
424
|
font-family: var(--input-font-family, inherit);
|
|
@@ -408,6 +426,12 @@
|
|
|
408
426
|
outline: none;
|
|
409
427
|
padding: var(--input-padding, 16px);
|
|
410
428
|
font-weight: var(--input-font-weight, 500);
|
|
429
|
+
|
|
430
|
+
/* `normal` is what a textarea/input computes today regardless of any inherited
|
|
431
|
+
value — the UA sheet sets it, and inheritance loses to a UA declaration on the
|
|
432
|
+
element itself. So the default here is byte-identical for existing consumers,
|
|
433
|
+
and this is the only way a consumer can set it at all. */
|
|
434
|
+
line-height: var(--input-line-height, normal);
|
|
411
435
|
width: var(--input-width, fit-content);
|
|
412
436
|
margin: var(--input-margin, 0);
|
|
413
437
|
appearance: none !important;
|
|
@@ -29,7 +29,12 @@ export type OptionalInputProperties = {
|
|
|
29
29
|
validationPattern?: RegExp | null;
|
|
30
30
|
inProgressPattern?: RegExp | null;
|
|
31
31
|
addFocusColor?: boolean;
|
|
32
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Native `maxlength`. Defaults to 1000. Pass `null` for no limit — a composer or
|
|
34
|
+
* paste target that silently truncates long input is worse than an unbounded one,
|
|
35
|
+
* and the attribute is rendered unconditionally otherwise.
|
|
36
|
+
*/
|
|
37
|
+
maxLength?: number | null;
|
|
33
38
|
minLength?: number;
|
|
34
39
|
min?: number;
|
|
35
40
|
max?: number;
|
|
@@ -228,6 +228,10 @@
|
|
|
228
228
|
})
|
|
229
229
|
);
|
|
230
230
|
|
|
231
|
+
// Markers paint back-to-front so the first series ends up on top — see the
|
|
232
|
+
// comment on the marker loop in the markup.
|
|
233
|
+
let markerPaintOrder = $derived(lines.map((_line, index) => index).reverse());
|
|
234
|
+
|
|
231
235
|
let legendItems = $derived<LegendItem[]>(
|
|
232
236
|
series.map((s, i) => ({
|
|
233
237
|
label: s.name,
|
|
@@ -658,6 +662,38 @@
|
|
|
658
662
|
stroke-dasharray={line.dash}
|
|
659
663
|
fill="none"
|
|
660
664
|
/>
|
|
665
|
+
{#if showValues && pointLabelPlacements[si]}
|
|
666
|
+
{#each line.points as _point, pi (pi)}
|
|
667
|
+
{@const pl = pointLabelPlacements[si][pi]}
|
|
668
|
+
{#if pl?.visible && Number.isFinite(pl.x) && Number.isFinite(pl.y)}
|
|
669
|
+
<text
|
|
670
|
+
class="point-value"
|
|
671
|
+
x={pl.x}
|
|
672
|
+
y={pl.y}
|
|
673
|
+
text-anchor="middle"
|
|
674
|
+
dominant-baseline={pl.dominantBaseline}
|
|
675
|
+
>{formatNumber(series[si].data[pi].y)}</text
|
|
676
|
+
>
|
|
677
|
+
{/if}
|
|
678
|
+
{/each}
|
|
679
|
+
{/if}
|
|
680
|
+
{/if}
|
|
681
|
+
{/each}
|
|
682
|
+
|
|
683
|
+
<!--
|
|
684
|
+
Point markers are painted after every line, and in REVERSE series
|
|
685
|
+
order. Two series that share a value put their markers on identical
|
|
686
|
+
coordinates, and SVG has no z-index — whichever is written last wins.
|
|
687
|
+
Painting forwards meant the last series in the array hid the first,
|
|
688
|
+
so a chart passed [primary, comparison] lost the primary series'
|
|
689
|
+
marker wherever the two periods happened to agree, most visibly at
|
|
690
|
+
the first bucket. Reversing here makes the FIRST series win, which is
|
|
691
|
+
the one a reader is looking at; the legend keeps its original order
|
|
692
|
+
because it is derived from `series`, not from this loop.
|
|
693
|
+
-->
|
|
694
|
+
{#each markerPaintOrder as si (si)}
|
|
695
|
+
{@const line = lines[si]}
|
|
696
|
+
{#if !line.hidden}
|
|
661
697
|
{#if line.points.length === 1 && !showDots && Number.isFinite(line.points[0].x) && Number.isFinite(line.points[0].y)}
|
|
662
698
|
<circle
|
|
663
699
|
class="single-point"
|
|
@@ -684,21 +720,6 @@
|
|
|
684
720
|
{/if}
|
|
685
721
|
{/each}
|
|
686
722
|
{/if}
|
|
687
|
-
{#if showValues && pointLabelPlacements[si]}
|
|
688
|
-
{#each line.points as _point, pi (pi)}
|
|
689
|
-
{@const pl = pointLabelPlacements[si][pi]}
|
|
690
|
-
{#if pl?.visible && Number.isFinite(pl.x) && Number.isFinite(pl.y)}
|
|
691
|
-
<text
|
|
692
|
-
class="point-value"
|
|
693
|
-
x={pl.x}
|
|
694
|
-
y={pl.y}
|
|
695
|
-
text-anchor="middle"
|
|
696
|
-
dominant-baseline={pl.dominantBaseline}
|
|
697
|
-
>{formatNumber(series[si].data[pi].y)}</text
|
|
698
|
-
>
|
|
699
|
-
{/if}
|
|
700
|
-
{/each}
|
|
701
|
-
{/if}
|
|
702
723
|
{/if}
|
|
703
724
|
{/each}
|
|
704
725
|
|
package/dist/Menu/Menu.svelte
CHANGED
|
@@ -200,9 +200,28 @@
|
|
|
200
200
|
focusedIndex = -1;
|
|
201
201
|
typeaheadQuery = '';
|
|
202
202
|
onclose?.();
|
|
203
|
-
|
|
204
|
-
|
|
203
|
+
focusTrigger();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Returns focus to whatever is actually focusable for this trigger. Under
|
|
208
|
+
* `interactiveTrigger` the wrapper carries no tabindex, so focusing it is a no-op and
|
|
209
|
+
* focus falls to <body> — the control the snippet rendered is the real target.
|
|
210
|
+
*/
|
|
211
|
+
function focusTrigger() {
|
|
212
|
+
if (triggerEl === null) {
|
|
213
|
+
return;
|
|
205
214
|
}
|
|
215
|
+
if (interactiveTrigger) {
|
|
216
|
+
const control = triggerEl.querySelector(
|
|
217
|
+
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
218
|
+
);
|
|
219
|
+
if (control instanceof HTMLElement) {
|
|
220
|
+
control.focus({ preventScroll: true });
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
triggerEl.focus({ preventScroll: true });
|
|
206
225
|
}
|
|
207
226
|
|
|
208
227
|
function selectItem(item: MenuItem) {
|
|
@@ -240,6 +259,23 @@
|
|
|
240
259
|
}
|
|
241
260
|
}
|
|
242
261
|
|
|
262
|
+
/**
|
|
263
|
+
* Keydown wiring handed to an `interactiveTrigger` snippet. Enter and Space are
|
|
264
|
+
* deliberately NOT handled here: the snippet owns a real `<button>`, which already
|
|
265
|
+
* synthesises a click from both, and that click is already wired to `toggle`. Handling
|
|
266
|
+
* them again would open a menu the click then immediately closes. Only the arrow keys —
|
|
267
|
+
* which no native button implements — are added.
|
|
268
|
+
*/
|
|
269
|
+
function handleInteractiveTriggerKeydown(event: KeyboardEvent) {
|
|
270
|
+
if (event.key === 'ArrowDown') {
|
|
271
|
+
event.preventDefault();
|
|
272
|
+
openMenu();
|
|
273
|
+
} else if (event.key === 'ArrowUp') {
|
|
274
|
+
event.preventDefault();
|
|
275
|
+
openMenu(selectableItems.length - 1);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
243
279
|
function handleMenuKeydown(event: KeyboardEvent) {
|
|
244
280
|
switch (event.key) {
|
|
245
281
|
case 'ArrowDown': {
|
|
@@ -358,7 +394,7 @@
|
|
|
358
394
|
{#if typeof trigger === 'function'}
|
|
359
395
|
{@render trigger({
|
|
360
396
|
onclick: toggle,
|
|
361
|
-
onkeydown:
|
|
397
|
+
onkeydown: handleInteractiveTriggerKeydown,
|
|
362
398
|
ariaHaspopup: 'menu',
|
|
363
399
|
ariaExpanded: open
|
|
364
400
|
})}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ResizableProperties, ResizeEdge } from './properties';
|
|
3
|
+
|
|
4
|
+
let {
|
|
5
|
+
width = $bindable(null),
|
|
6
|
+
height = $bindable(null),
|
|
7
|
+
minWidth = 0,
|
|
8
|
+
maxWidth = Number.POSITIVE_INFINITY,
|
|
9
|
+
minHeight = 0,
|
|
10
|
+
maxHeight = Number.POSITIVE_INFINITY,
|
|
11
|
+
handles = ['bottom-right'],
|
|
12
|
+
step = 16,
|
|
13
|
+
disabled = false,
|
|
14
|
+
handleLabel = 'Resize',
|
|
15
|
+
children,
|
|
16
|
+
onresize,
|
|
17
|
+
onresizestart,
|
|
18
|
+
onresizeend,
|
|
19
|
+
testId,
|
|
20
|
+
classes
|
|
21
|
+
}: ResizableProperties = $props();
|
|
22
|
+
|
|
23
|
+
const DIRS: Record<ResizeEdge, { x: -1 | 0 | 1; y: -1 | 0 | 1 }> = {
|
|
24
|
+
top: { x: 0, y: -1 },
|
|
25
|
+
right: { x: 1, y: 0 },
|
|
26
|
+
bottom: { x: 0, y: 1 },
|
|
27
|
+
left: { x: -1, y: 0 },
|
|
28
|
+
'top-left': { x: -1, y: -1 },
|
|
29
|
+
'top-right': { x: 1, y: -1 },
|
|
30
|
+
'bottom-left': { x: -1, y: 1 },
|
|
31
|
+
'bottom-right': { x: 1, y: 1 }
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
let container: HTMLElement | null = $state(null);
|
|
35
|
+
let active: {
|
|
36
|
+
edge: ResizeEdge;
|
|
37
|
+
pointerId: number;
|
|
38
|
+
startX: number;
|
|
39
|
+
startY: number;
|
|
40
|
+
startW: number;
|
|
41
|
+
startH: number;
|
|
42
|
+
} | null = $state(null);
|
|
43
|
+
|
|
44
|
+
let activeHandles = $derived(disabled ? [] : handles);
|
|
45
|
+
|
|
46
|
+
function clampWidth(value: number): number {
|
|
47
|
+
return Math.min(maxWidth, Math.max(minWidth, value));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function clampHeight(value: number): number {
|
|
51
|
+
return Math.min(maxHeight, Math.max(minHeight, value));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function measuredWidth(): number {
|
|
55
|
+
return typeof width === 'number' ? width : (container?.offsetWidth ?? 0);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function measuredHeight(): number {
|
|
59
|
+
return typeof height === 'number' ? height : (container?.offsetHeight ?? 0);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function axisOf(edge: ResizeEdge): 'x' | 'y' | 'both' {
|
|
63
|
+
const dir = DIRS[edge];
|
|
64
|
+
if (dir.x !== 0 && dir.y !== 0) {
|
|
65
|
+
return 'both';
|
|
66
|
+
}
|
|
67
|
+
return dir.x !== 0 ? 'x' : 'y';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function orientationOf(edge: ResizeEdge): 'horizontal' | 'vertical' | null {
|
|
71
|
+
const axis = axisOf(edge);
|
|
72
|
+
if (axis === 'x') {
|
|
73
|
+
return 'vertical';
|
|
74
|
+
}
|
|
75
|
+
if (axis === 'y') {
|
|
76
|
+
return 'horizontal';
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function startResize(
|
|
82
|
+
event: PointerEvent & { currentTarget: HTMLElement },
|
|
83
|
+
edge: ResizeEdge
|
|
84
|
+
): void {
|
|
85
|
+
if (disabled) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
event.preventDefault();
|
|
89
|
+
const startW = clampWidth(measuredWidth());
|
|
90
|
+
const startH = clampHeight(measuredHeight());
|
|
91
|
+
width = startW;
|
|
92
|
+
height = startH;
|
|
93
|
+
active = {
|
|
94
|
+
edge,
|
|
95
|
+
pointerId: event.pointerId,
|
|
96
|
+
startX: event.clientX,
|
|
97
|
+
startY: event.clientY,
|
|
98
|
+
startW,
|
|
99
|
+
startH
|
|
100
|
+
};
|
|
101
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
102
|
+
onresizestart?.({ width: startW, height: startH });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function moveResize(event: PointerEvent): void {
|
|
106
|
+
if (active === null) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const dir = DIRS[active.edge];
|
|
110
|
+
if (dir.x !== 0) {
|
|
111
|
+
width = clampWidth(active.startW + dir.x * (event.clientX - active.startX));
|
|
112
|
+
}
|
|
113
|
+
if (dir.y !== 0) {
|
|
114
|
+
height = clampHeight(active.startH + dir.y * (event.clientY - active.startY));
|
|
115
|
+
}
|
|
116
|
+
onresize?.({ width: measuredWidth(), height: measuredHeight() });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function endResize(event: PointerEvent & { currentTarget: HTMLElement }): void {
|
|
120
|
+
if (active === null) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
|
|
124
|
+
event.currentTarget.releasePointerCapture(event.pointerId);
|
|
125
|
+
}
|
|
126
|
+
active = null;
|
|
127
|
+
onresizeend?.({ width: measuredWidth(), height: measuredHeight() });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function keyResize(event: KeyboardEvent, edge: ResizeEdge): void {
|
|
131
|
+
if (disabled) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const dir = DIRS[edge];
|
|
135
|
+
let nextWidth = measuredWidth();
|
|
136
|
+
let nextHeight = measuredHeight();
|
|
137
|
+
let handled = false;
|
|
138
|
+
|
|
139
|
+
if (dir.x !== 0 && (event.key === 'ArrowRight' || event.key === 'ArrowLeft')) {
|
|
140
|
+
nextWidth = clampWidth(nextWidth + (event.key === 'ArrowRight' ? step : -step));
|
|
141
|
+
handled = true;
|
|
142
|
+
}
|
|
143
|
+
if (dir.y !== 0 && (event.key === 'ArrowDown' || event.key === 'ArrowUp')) {
|
|
144
|
+
nextHeight = clampHeight(nextHeight + (event.key === 'ArrowDown' ? step : -step));
|
|
145
|
+
handled = true;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (handled) {
|
|
149
|
+
event.preventDefault();
|
|
150
|
+
width = nextWidth;
|
|
151
|
+
height = nextHeight;
|
|
152
|
+
onresize?.({ width: nextWidth, height: nextHeight });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function valueNow(edge: ResizeEdge): number | null {
|
|
157
|
+
const axis = axisOf(edge);
|
|
158
|
+
if (axis === 'y') {
|
|
159
|
+
return typeof height === 'number' ? height : null;
|
|
160
|
+
}
|
|
161
|
+
return typeof width === 'number' ? width : null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function valueMin(edge: ResizeEdge): number {
|
|
165
|
+
return axisOf(edge) === 'y' ? minHeight : minWidth;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function valueMax(edge: ResizeEdge): number | null {
|
|
169
|
+
const max = axisOf(edge) === 'y' ? maxHeight : maxWidth;
|
|
170
|
+
return Number.isFinite(max) ? max : null;
|
|
171
|
+
}
|
|
172
|
+
</script>
|
|
173
|
+
|
|
174
|
+
<div
|
|
175
|
+
class="resizable {classes ?? ''}"
|
|
176
|
+
class:resizing={active !== null}
|
|
177
|
+
data-pw={typeof testId === 'string' ? testId : null}
|
|
178
|
+
testID={typeof testId === 'string' ? testId : null}
|
|
179
|
+
bind:this={container}
|
|
180
|
+
style:width={typeof width === 'number' ? `${width}px` : null}
|
|
181
|
+
style:height={typeof height === 'number' ? `${height}px` : null}
|
|
182
|
+
>
|
|
183
|
+
{#if typeof children === 'function'}
|
|
184
|
+
{@render children()}
|
|
185
|
+
{/if}
|
|
186
|
+
|
|
187
|
+
{#each activeHandles as edge (edge)}
|
|
188
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
189
|
+
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
190
|
+
<div
|
|
191
|
+
class="handle"
|
|
192
|
+
data-edge={edge}
|
|
193
|
+
role="separator"
|
|
194
|
+
tabindex="0"
|
|
195
|
+
aria-label={handleLabel}
|
|
196
|
+
aria-orientation={orientationOf(edge)}
|
|
197
|
+
aria-valuenow={valueNow(edge)}
|
|
198
|
+
aria-valuemin={valueMin(edge)}
|
|
199
|
+
aria-valuemax={valueMax(edge)}
|
|
200
|
+
onpointerdown={(event) => startResize(event, edge)}
|
|
201
|
+
onpointermove={moveResize}
|
|
202
|
+
onpointerup={endResize}
|
|
203
|
+
onkeydown={(event) => keyResize(event, edge)}
|
|
204
|
+
></div>
|
|
205
|
+
{/each}
|
|
206
|
+
</div>
|
|
207
|
+
|
|
208
|
+
<style>
|
|
209
|
+
.resizable {
|
|
210
|
+
position: relative;
|
|
211
|
+
box-sizing: border-box;
|
|
212
|
+
max-width: var(--resizable-max-width, none);
|
|
213
|
+
max-height: var(--resizable-max-height, none);
|
|
214
|
+
transition: var(--resizable-transition, none);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.resizable.resizing {
|
|
218
|
+
user-select: none;
|
|
219
|
+
transition: none;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
@media (prefers-reduced-motion: reduce) {
|
|
223
|
+
.resizable {
|
|
224
|
+
transition: none;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.handle {
|
|
229
|
+
position: absolute;
|
|
230
|
+
touch-action: none;
|
|
231
|
+
z-index: var(--resizable-handle-z-index, 2);
|
|
232
|
+
background: var(--resizable-handle-color, transparent);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.handle:focus-visible {
|
|
236
|
+
outline: var(--resizable-handle-focus-outline, 2px solid #3b5bdb);
|
|
237
|
+
outline-offset: var(--resizable-handle-focus-outline-offset, -2px);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.handle[data-edge='top'],
|
|
241
|
+
.handle[data-edge='bottom'] {
|
|
242
|
+
left: 0;
|
|
243
|
+
right: 0;
|
|
244
|
+
height: var(--resizable-edge-size, 8px);
|
|
245
|
+
cursor: ns-resize;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.handle[data-edge='left'],
|
|
249
|
+
.handle[data-edge='right'] {
|
|
250
|
+
top: 0;
|
|
251
|
+
bottom: 0;
|
|
252
|
+
width: var(--resizable-edge-size, 8px);
|
|
253
|
+
cursor: ew-resize;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.handle[data-edge='top'] {
|
|
257
|
+
top: 0;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.handle[data-edge='bottom'] {
|
|
261
|
+
bottom: 0;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.handle[data-edge='left'] {
|
|
265
|
+
left: 0;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.handle[data-edge='right'] {
|
|
269
|
+
right: 0;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.handle[data-edge='top-left'],
|
|
273
|
+
.handle[data-edge='top-right'],
|
|
274
|
+
.handle[data-edge='bottom-left'],
|
|
275
|
+
.handle[data-edge='bottom-right'] {
|
|
276
|
+
width: var(--resizable-corner-size, 14px);
|
|
277
|
+
height: var(--resizable-corner-size, 14px);
|
|
278
|
+
z-index: var(--resizable-corner-z-index, 3);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.handle[data-edge='top-left'] {
|
|
282
|
+
top: 0;
|
|
283
|
+
left: 0;
|
|
284
|
+
cursor: nwse-resize;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.handle[data-edge='top-right'] {
|
|
288
|
+
top: 0;
|
|
289
|
+
right: 0;
|
|
290
|
+
cursor: nesw-resize;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.handle[data-edge='bottom-left'] {
|
|
294
|
+
bottom: 0;
|
|
295
|
+
left: 0;
|
|
296
|
+
cursor: nesw-resize;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.handle[data-edge='bottom-right'] {
|
|
300
|
+
bottom: 0;
|
|
301
|
+
right: 0;
|
|
302
|
+
cursor: nwse-resize;
|
|
303
|
+
}
|
|
304
|
+
</style>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
export type ResizeEdge = 'top' | 'right' | 'bottom' | 'left' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
3
|
+
export type ResizeSize = {
|
|
4
|
+
width: number;
|
|
5
|
+
height: number;
|
|
6
|
+
};
|
|
7
|
+
export type ResizableProperties = OptionalResizableProperties & ResizableEventProperties;
|
|
8
|
+
export type OptionalResizableProperties = {
|
|
9
|
+
width?: number | null;
|
|
10
|
+
height?: number | null;
|
|
11
|
+
minWidth?: number;
|
|
12
|
+
maxWidth?: number;
|
|
13
|
+
minHeight?: number;
|
|
14
|
+
maxHeight?: number;
|
|
15
|
+
handles?: ResizeEdge[];
|
|
16
|
+
step?: number;
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
handleLabel?: string;
|
|
19
|
+
children?: Snippet;
|
|
20
|
+
testId?: string;
|
|
21
|
+
classes?: string;
|
|
22
|
+
};
|
|
23
|
+
export type ResizableEventProperties = {
|
|
24
|
+
onresize?: (size: ResizeSize) => void;
|
|
25
|
+
onresizestart?: (size: ResizeSize) => void;
|
|
26
|
+
onresizeend?: (size: ResizeSize) => void;
|
|
27
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" />
|
|
3
|
+
</svg>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<rect x="9" y="2" width="6" height="12" rx="3" />
|
|
3
|
+
<path d="M5 11a7 7 0 0 0 14 0" />
|
|
4
|
+
<line x1="12" y1="18" x2="12" y2="22" />
|
|
5
|
+
</svg>
|