@coyalabs/bts-style 1.3.15 → 1.3.18
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/README.md +58 -8
- package/dist/Base/BaseText.svelte +3 -2
- package/dist/Components/ContextMenu.svelte +175 -29
- package/dist/Components/InputBox.svelte +241 -18
- package/dist/Components/InputBox.svelte.d.ts +23 -1
- package/dist/Components/Popup/AlertPopup.svelte +8 -0
- package/dist/Components/Popup/AlertPopup.svelte.d.ts +11 -1
- package/dist/Components/Popup/ConfirmPopup.svelte +8 -0
- package/dist/Components/Popup/ConfirmPopup.svelte.d.ts +11 -1
- package/dist/Components/Popup/Popup.svelte +90 -3
- package/dist/Components/Popup/PromptPopup.svelte +16 -1
- package/dist/Components/Popup/PromptPopup.svelte.d.ts +15 -1
- package/dist/Components/Popup/popupStore.d.ts +12 -0
- package/dist/Components/Popup/popupStore.js +57 -29
- package/dist/Components/ScrollContainer.svelte +88 -3
- package/dist/Components/ScrollContainer.svelte.d.ts +2 -0
- package/dist/icons.d.ts +1 -0
- package/dist/icons.js +2 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -340,6 +340,11 @@ Text input field with icon support and theme matching.
|
|
|
340
340
|
- `value?: string` - Input value (bindable)
|
|
341
341
|
- `placeholder?: string` - Placeholder text
|
|
342
342
|
- `type?: string` - Input type (default: `'text'`)
|
|
343
|
+
- `multiline?: boolean` - Render a textarea instead of an input (default: `false`)
|
|
344
|
+
- `maxRows?: number` - Maximum visible rows for auto-growing multiline inputs (default: `4`)
|
|
345
|
+
- `expandOnNewline?: boolean` - Keep a multiline input visually single-line until the value contains a newline (default: `false`)
|
|
346
|
+
- `newlineOnCtrlEnter?: boolean` - For multiline inputs, plain Enter submits the nearest form or bubbles to parent handlers, while `Ctrl+Enter` and `Shift+Enter` insert a newline (default: `false`)
|
|
347
|
+
- `resize?: 'none' | 'both' | 'horizontal' | 'vertical'` - Textarea resize mode when `multiline` is enabled (default: `'vertical'`)
|
|
343
348
|
- `theme?: 'full' | 'primary' | 'secondary' | 'filled'` - Visual theme
|
|
344
349
|
- `icon?: string` - Left icon SVG
|
|
345
350
|
- All BaseContainer corner radius props
|
|
@@ -357,6 +362,33 @@ Text input field with icon support and theme matching.
|
|
|
357
362
|
icon={icons.pen}
|
|
358
363
|
theme="primary"
|
|
359
364
|
/>
|
|
365
|
+
|
|
366
|
+
<InputBox
|
|
367
|
+
bind:value={username}
|
|
368
|
+
placeholder="Write a longer note..."
|
|
369
|
+
icon={icons.pen}
|
|
370
|
+
multiline={true}
|
|
371
|
+
maxRows={6}
|
|
372
|
+
resize="vertical"
|
|
373
|
+
/>
|
|
374
|
+
|
|
375
|
+
<InputBox
|
|
376
|
+
bind:value={username}
|
|
377
|
+
placeholder="Looks like one line until you press Enter..."
|
|
378
|
+
icon={icons.pen}
|
|
379
|
+
multiline={true}
|
|
380
|
+
expandOnNewline={true}
|
|
381
|
+
maxRows={4}
|
|
382
|
+
/>
|
|
383
|
+
|
|
384
|
+
<InputBox
|
|
385
|
+
bind:value={username}
|
|
386
|
+
placeholder="Press Enter to submit, Ctrl+Enter for a newline"
|
|
387
|
+
icon={icons.pen}
|
|
388
|
+
multiline={true}
|
|
389
|
+
newlineOnCtrlEnter={true}
|
|
390
|
+
maxRows={4}
|
|
391
|
+
/>
|
|
360
392
|
```
|
|
361
393
|
|
|
362
394
|
---
|
|
@@ -429,6 +461,7 @@ Reusable overflow wrapper with theme-matching custom scrollbar styling.
|
|
|
429
461
|
- `thumbColor?: string` - Scrollbar thumb color (default: `'rgba(161, 143, 143, 0.3)'`)
|
|
430
462
|
- `thumbHoverColor?: string` - Scrollbar thumb hover color (default: `'rgba(161, 143, 143, 0.5)'`)
|
|
431
463
|
- `borderRadius?: string` - Scrollbar track/thumb radius (default: `'4px'`)
|
|
464
|
+
- `hideScrollbarUntilScroll?: boolean` - Hide scrollbars until the container has been scrolled from its initial position (default: `false`)
|
|
432
465
|
|
|
433
466
|
**Example:**
|
|
434
467
|
```svelte
|
|
@@ -441,6 +474,12 @@ Reusable overflow wrapper with theme-matching custom scrollbar styling.
|
|
|
441
474
|
Scrollable content with BTS-themed scrollbar.
|
|
442
475
|
</div>
|
|
443
476
|
</ScrollContainer>
|
|
477
|
+
|
|
478
|
+
<ScrollContainer overflowY="auto" maxHeight="320px" hideScrollbarUntilScroll={true}>
|
|
479
|
+
<div style="min-height: 640px;">
|
|
480
|
+
Scrollbars stay hidden until the first real scroll.
|
|
481
|
+
</div>
|
|
482
|
+
</ScrollContainer>
|
|
444
483
|
```
|
|
445
484
|
|
|
446
485
|
**Features:**
|
|
@@ -779,13 +818,18 @@ Writable store for controlling the popup.
|
|
|
779
818
|
popupStore.open(
|
|
780
819
|
title: string,
|
|
781
820
|
component: SvelteComponent,
|
|
782
|
-
props?: object
|
|
821
|
+
props?: object & {
|
|
822
|
+
popupWidthRatio?: number
|
|
823
|
+
},
|
|
783
824
|
subtitle?: string
|
|
784
825
|
)
|
|
785
826
|
```
|
|
786
827
|
|
|
787
828
|
Opens popup with custom component.
|
|
788
829
|
|
|
830
|
+
`popupWidthRatio` multiplies the default popup width while keeping the dialog capped to the viewport.
|
|
831
|
+
Use `1` for the current width, values below `1` for narrower dialogs, and values above `1` for wider dialogs.
|
|
832
|
+
|
|
789
833
|
**Example:**
|
|
790
834
|
```svelte
|
|
791
835
|
<script>
|
|
@@ -797,7 +841,7 @@ Opens popup with custom component.
|
|
|
797
841
|
popupStore.open(
|
|
798
842
|
'Settings',
|
|
799
843
|
MyCustomPopup,
|
|
800
|
-
{ userId: 123 },
|
|
844
|
+
{ userId: 123, popupWidthRatio: 1.25 },
|
|
801
845
|
'Configure your preferences'
|
|
802
846
|
)
|
|
803
847
|
}>
|
|
@@ -814,7 +858,8 @@ popupStore.confirm(
|
|
|
814
858
|
onConfirm?: () => void,
|
|
815
859
|
onCancel?: () => void,
|
|
816
860
|
confirmText?: string,
|
|
817
|
-
cancelText?: string
|
|
861
|
+
cancelText?: string,
|
|
862
|
+
popupWidthRatio?: number
|
|
818
863
|
}
|
|
819
864
|
)
|
|
820
865
|
```
|
|
@@ -831,7 +876,8 @@ Shows confirmation dialog.
|
|
|
831
876
|
onConfirm: () => deleteItem(),
|
|
832
877
|
onCancel: () => console.log('Cancelled'),
|
|
833
878
|
confirmText: 'Delete',
|
|
834
|
-
cancelText: 'Keep'
|
|
879
|
+
cancelText: 'Keep',
|
|
880
|
+
popupWidthRatio: 1.15
|
|
835
881
|
}
|
|
836
882
|
)
|
|
837
883
|
}>
|
|
@@ -846,7 +892,8 @@ popupStore.alert(
|
|
|
846
892
|
message: string,
|
|
847
893
|
options?: {
|
|
848
894
|
onOk?: () => void,
|
|
849
|
-
okText?: string
|
|
895
|
+
okText?: string,
|
|
896
|
+
popupWidthRatio?: number
|
|
850
897
|
}
|
|
851
898
|
)
|
|
852
899
|
```
|
|
@@ -860,7 +907,8 @@ popupStore.alert(
|
|
|
860
907
|
'Your changes have been saved!',
|
|
861
908
|
{
|
|
862
909
|
onOk: () => navigateToHome(),
|
|
863
|
-
okText: 'Got it'
|
|
910
|
+
okText: 'Got it',
|
|
911
|
+
popupWidthRatio: 0.9
|
|
864
912
|
}
|
|
865
913
|
);
|
|
866
914
|
```
|
|
@@ -875,7 +923,8 @@ popupStore.prompt(
|
|
|
875
923
|
onCancel?: () => void,
|
|
876
924
|
placeholder?: string,
|
|
877
925
|
submitText?: string,
|
|
878
|
-
cancelText?: string
|
|
926
|
+
cancelText?: string,
|
|
927
|
+
popupWidthRatio?: number
|
|
879
928
|
}
|
|
880
929
|
)
|
|
881
930
|
```
|
|
@@ -891,7 +940,8 @@ popupStore.prompt(
|
|
|
891
940
|
onSubmit: (name) => updateProfile(name),
|
|
892
941
|
placeholder: 'Your name...',
|
|
893
942
|
submitText: 'Save',
|
|
894
|
-
cancelText: 'Skip'
|
|
943
|
+
cancelText: 'Skip',
|
|
944
|
+
popupWidthRatio: 0.95
|
|
895
945
|
}
|
|
896
946
|
);
|
|
897
947
|
```
|
|
@@ -35,7 +35,8 @@
|
|
|
35
35
|
|
|
36
36
|
$: usesContent = content !== null && content !== undefined;
|
|
37
37
|
$: tagName = as || (markdown && usesContent ? 'div' : 'span');
|
|
38
|
-
|
|
38
|
+
// @ts-ignore
|
|
39
|
+
$: renderedContent = markdown && usesContent ? renderMarkdown(content) : '';
|
|
39
40
|
</script>
|
|
40
41
|
|
|
41
42
|
<svelte:element
|
|
@@ -122,7 +123,7 @@
|
|
|
122
123
|
color: #C7BDC1;
|
|
123
124
|
}
|
|
124
125
|
.text[data-markdown='true'] :global(a) {
|
|
125
|
-
color: #
|
|
126
|
+
color: #67abff;
|
|
126
127
|
text-decoration: underline;
|
|
127
128
|
text-decoration-thickness: 1px;
|
|
128
129
|
text-underline-offset: 0.14em;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script>
|
|
2
|
+
import { onMount, tick } from 'svelte';
|
|
2
3
|
import { expoOut } from 'svelte/easing';
|
|
3
4
|
import BaseContainer from '../Base/BaseContainer.svelte';
|
|
4
5
|
import BaseText from '../Base/BaseText.svelte';
|
|
@@ -33,6 +34,68 @@
|
|
|
33
34
|
'bottom-right': 'bottom right'
|
|
34
35
|
};
|
|
35
36
|
|
|
37
|
+
const VIEWPORT_PADDING = 12;
|
|
38
|
+
|
|
39
|
+
/** @type {HTMLDivElement | null} */
|
|
40
|
+
let anchorRef = null;
|
|
41
|
+
|
|
42
|
+
let viewportOffsetX = 0;
|
|
43
|
+
let viewportOffsetY = 0;
|
|
44
|
+
|
|
45
|
+
/** @type {number | null} */
|
|
46
|
+
let updateFrame = null;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @param {number} position
|
|
50
|
+
* @param {number} size
|
|
51
|
+
* @param {number} viewportSize
|
|
52
|
+
*/
|
|
53
|
+
function getViewportOffset(position, size, viewportSize) {
|
|
54
|
+
const viewportMin = VIEWPORT_PADDING;
|
|
55
|
+
const viewportMax = viewportSize - VIEWPORT_PADDING;
|
|
56
|
+
const availableSize = Math.max(viewportSize - VIEWPORT_PADDING * 2, 0);
|
|
57
|
+
|
|
58
|
+
if (size >= availableSize) {
|
|
59
|
+
return viewportMin - position;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (position < viewportMin) {
|
|
63
|
+
return viewportMin - position;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const end = position + size;
|
|
67
|
+
if (end > viewportMax) {
|
|
68
|
+
return viewportMax - end;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function updateViewportOffset() {
|
|
75
|
+
if (!anchorRef || typeof window === 'undefined') {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const rect = anchorRef.getBoundingClientRect();
|
|
80
|
+
viewportOffsetX = getViewportOffset(rect.left, rect.width, window.innerWidth);
|
|
81
|
+
viewportOffsetY = getViewportOffset(rect.top, rect.height, window.innerHeight);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function scheduleViewportOffsetUpdate() {
|
|
85
|
+
if (typeof window === 'undefined') {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (updateFrame !== null) {
|
|
90
|
+
cancelAnimationFrame(updateFrame);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
updateFrame = requestAnimationFrame(() => {
|
|
94
|
+
updateFrame = null;
|
|
95
|
+
updateViewportOffset();
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
36
99
|
/**
|
|
37
100
|
* Custom scale transition with independent x and y animations
|
|
38
101
|
* @param {HTMLElement} node
|
|
@@ -91,45 +154,128 @@
|
|
|
91
154
|
function handleSelect(value) {
|
|
92
155
|
onSelect(value);
|
|
93
156
|
}
|
|
157
|
+
|
|
158
|
+
onMount(() => {
|
|
159
|
+
scheduleViewportOffsetUpdate();
|
|
160
|
+
void tick().then(() => {
|
|
161
|
+
scheduleViewportOffsetUpdate();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
/** @type {ResizeObserver | null} */
|
|
165
|
+
let resizeObserver = null;
|
|
166
|
+
|
|
167
|
+
if (typeof ResizeObserver !== 'undefined' && anchorRef) {
|
|
168
|
+
resizeObserver = new ResizeObserver(() => {
|
|
169
|
+
scheduleViewportOffsetUpdate();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
resizeObserver.observe(anchorRef);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
window.addEventListener('resize', scheduleViewportOffsetUpdate);
|
|
176
|
+
window.addEventListener('scroll', scheduleViewportOffsetUpdate, true);
|
|
177
|
+
|
|
178
|
+
return () => {
|
|
179
|
+
resizeObserver?.disconnect();
|
|
180
|
+
window.removeEventListener('resize', scheduleViewportOffsetUpdate);
|
|
181
|
+
window.removeEventListener('scroll', scheduleViewportOffsetUpdate, true);
|
|
182
|
+
|
|
183
|
+
if (updateFrame !== null) {
|
|
184
|
+
cancelAnimationFrame(updateFrame);
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
});
|
|
94
188
|
</script>
|
|
95
189
|
|
|
96
|
-
<div
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
190
|
+
<div class="context-menu-anchor" bind:this={anchorRef}>
|
|
191
|
+
<div
|
|
192
|
+
class="context-menu-clamp"
|
|
193
|
+
style="
|
|
194
|
+
--context-menu-offset-x: {viewportOffsetX}px;
|
|
195
|
+
--context-menu-offset-y: {viewportOffsetY}px;
|
|
196
|
+
--context-menu-max-width: calc(100vw - {VIEWPORT_PADDING * 2}px);
|
|
197
|
+
--context-menu-max-height: calc(100vh - {VIEWPORT_PADDING * 2}px);
|
|
198
|
+
"
|
|
199
|
+
>
|
|
200
|
+
<div
|
|
201
|
+
class="context-menu-wrapper"
|
|
202
|
+
style:transform-origin={originMap[origin]}
|
|
203
|
+
in:scaleIn
|
|
204
|
+
out:scaleOut
|
|
205
|
+
>
|
|
206
|
+
<div class="context-menu-surface">
|
|
207
|
+
<BaseContainer theme="filled" padding="0.5rem" borderRadiusTopLeft="28px" borderRadiusTopRight="28px" borderRadiusBottomLeft="28px" borderRadiusBottomRight="28px">
|
|
208
|
+
<div style="height: 0.3rem;"></div>
|
|
209
|
+
{#each categories as category, catIndex}
|
|
210
|
+
{#if category.label}
|
|
211
|
+
<div class="separator" class:not-first={catIndex > 0}>
|
|
212
|
+
<BaseText textModifier="-4px" variant="button">{category.label}</BaseText>
|
|
213
|
+
</div>
|
|
214
|
+
{/if}
|
|
215
|
+
<div class="category-items">
|
|
216
|
+
{#each category.items as {item}}
|
|
217
|
+
<button
|
|
218
|
+
class="context-item"
|
|
219
|
+
class:selected={item.value === selectedValue}
|
|
220
|
+
class:disabled={item.disabled}
|
|
221
|
+
disabled={item.disabled}
|
|
222
|
+
on:click={() => handleSelect(item.value)}
|
|
223
|
+
>
|
|
224
|
+
{item.label}
|
|
225
|
+
</button>
|
|
226
|
+
{/each}
|
|
227
|
+
</div>
|
|
228
|
+
{/each}
|
|
229
|
+
<div style="height: 0.3rem;"></div>
|
|
230
|
+
</BaseContainer>
|
|
122
231
|
</div>
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
</BaseContainer>
|
|
232
|
+
</div>
|
|
233
|
+
</div>
|
|
126
234
|
</div>
|
|
127
235
|
|
|
128
236
|
<style>
|
|
237
|
+
.context-menu-anchor {
|
|
238
|
+
display: inline-block;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.context-menu-clamp {
|
|
242
|
+
display: inline-block;
|
|
243
|
+
transform: translate3d(var(--context-menu-offset-x), var(--context-menu-offset-y), 0);
|
|
244
|
+
}
|
|
245
|
+
|
|
129
246
|
.context-menu-wrapper {
|
|
130
247
|
display: inline-block;
|
|
131
248
|
}
|
|
132
249
|
|
|
250
|
+
.context-menu-surface {
|
|
251
|
+
display: inline-block;
|
|
252
|
+
max-width: var(--context-menu-max-width);
|
|
253
|
+
max-height: var(--context-menu-max-height);
|
|
254
|
+
overflow-x: hidden;
|
|
255
|
+
overflow-y: auto;
|
|
256
|
+
overscroll-behavior: contain;
|
|
257
|
+
border-radius: 28px;
|
|
258
|
+
scrollbar-width: thin;
|
|
259
|
+
scrollbar-color: rgba(161, 143, 143, 0.45) rgba(62, 53, 58, 0.35);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.context-menu-surface::-webkit-scrollbar {
|
|
263
|
+
width: 8px;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.context-menu-surface::-webkit-scrollbar-track {
|
|
267
|
+
background: rgba(62, 53, 58, 0.35);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.context-menu-surface::-webkit-scrollbar-thumb {
|
|
271
|
+
background: rgba(161, 143, 143, 0.45);
|
|
272
|
+
border-radius: 999px;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.context-menu-surface::-webkit-scrollbar-thumb:hover {
|
|
276
|
+
background: rgba(161, 143, 143, 0.65);
|
|
277
|
+
}
|
|
278
|
+
|
|
133
279
|
.category-items {
|
|
134
280
|
display: flex;
|
|
135
281
|
flex-direction: column;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script>
|
|
2
|
+
import { onMount, tick } from 'svelte';
|
|
2
3
|
import BaseContainer from '../Base/BaseContainer.svelte';
|
|
3
4
|
import BaseIcon from '../Base/BaseIcon.svelte';
|
|
4
5
|
import { icons } from '../icons.js';
|
|
@@ -28,6 +29,35 @@
|
|
|
28
29
|
*/
|
|
29
30
|
export let type = 'text';
|
|
30
31
|
|
|
32
|
+
/**
|
|
33
|
+
* @type {boolean}
|
|
34
|
+
*/
|
|
35
|
+
export let multiline = false;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @type {number}
|
|
39
|
+
*/
|
|
40
|
+
export let rows = 4;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Keep multiline inputs visually compact (1 row) until the value contains a newline.
|
|
44
|
+
* @type {boolean}
|
|
45
|
+
*/
|
|
46
|
+
export let expandOnNewline = false;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* When enabled for multiline inputs, plain Enter submits the nearest form
|
|
50
|
+
* or falls through to parent handlers, while Ctrl+Enter and Shift+Enter
|
|
51
|
+
* insert a newline.
|
|
52
|
+
* @type {boolean}
|
|
53
|
+
*/
|
|
54
|
+
export let newlineOnCtrlEnter = false;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @type {'none' | 'both' | 'horizontal' | 'vertical'}
|
|
58
|
+
*/
|
|
59
|
+
export let resize = 'vertical';
|
|
60
|
+
|
|
31
61
|
/**
|
|
32
62
|
* @type {string | null}
|
|
33
63
|
*/
|
|
@@ -62,27 +92,148 @@
|
|
|
62
92
|
* @type {boolean}
|
|
63
93
|
*/
|
|
64
94
|
export let autofocus = false;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* @type {boolean}
|
|
98
|
+
*/
|
|
99
|
+
export let disabled = false;
|
|
100
|
+
|
|
101
|
+
/** @type {HTMLInputElement | HTMLTextAreaElement | null} */
|
|
102
|
+
let inputRef = null;
|
|
103
|
+
|
|
104
|
+
let mounted = false;
|
|
105
|
+
|
|
106
|
+
let hasExplicitNewline = false;
|
|
107
|
+
|
|
108
|
+
/** @type {number | null} */
|
|
109
|
+
let focusFrame = null;
|
|
110
|
+
|
|
111
|
+
function clearScheduledFocus() {
|
|
112
|
+
if (focusFrame !== null) {
|
|
113
|
+
cancelAnimationFrame(focusFrame);
|
|
114
|
+
focusFrame = null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function scheduleFocus() {
|
|
119
|
+
if (!autofocus || disabled || !inputRef) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
clearScheduledFocus();
|
|
124
|
+
void tick().then(() => {
|
|
125
|
+
focusFrame = requestAnimationFrame(() => {
|
|
126
|
+
focusFrame = null;
|
|
127
|
+
inputRef?.focus({ preventScroll: true });
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
onMount(() => {
|
|
133
|
+
mounted = true;
|
|
134
|
+
scheduleFocus();
|
|
135
|
+
|
|
136
|
+
return () => {
|
|
137
|
+
clearScheduledFocus();
|
|
138
|
+
};
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
$: hasExplicitNewline = typeof value === 'string' && value.includes('\n');
|
|
142
|
+
$: effectiveRows = expandOnNewline && !hasExplicitNewline ? 1 : rows;
|
|
143
|
+
|
|
144
|
+
$: if (mounted && autofocus && inputRef) {
|
|
145
|
+
scheduleFocus();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function focus() {
|
|
149
|
+
if (disabled) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
inputRef?.focus({ preventScroll: true });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function blur() {
|
|
157
|
+
inputRef?.blur();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* @param {HTMLTextAreaElement} node
|
|
162
|
+
* @param {boolean} enabled
|
|
163
|
+
*/
|
|
164
|
+
function ctrlEnterNewline(node, enabled) {
|
|
165
|
+
let isEnabled = enabled;
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* @param {KeyboardEvent} event
|
|
169
|
+
*/
|
|
170
|
+
function handleKeydown(event) {
|
|
171
|
+
if (!isEnabled || event.key !== 'Enter' || event.isComposing || event.ctrlKey || event.metaKey || event.altKey || event.shiftKey) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
event.preventDefault();
|
|
176
|
+
node.form?.requestSubmit();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
node.addEventListener('keydown', handleKeydown);
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
update(/** @type {boolean} */ nextEnabled) {
|
|
183
|
+
isEnabled = nextEnabled;
|
|
184
|
+
},
|
|
185
|
+
destroy() {
|
|
186
|
+
node.removeEventListener('keydown', handleKeydown);
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
}
|
|
65
190
|
</script>
|
|
66
191
|
|
|
67
|
-
<div class="input-wrapper">
|
|
192
|
+
<div class="input-wrapper" class:disabled>
|
|
68
193
|
<BaseContainer {theme} {borderRadiusTopLeft} {borderRadiusTopRight} {borderRadiusBottomLeft} {borderRadiusBottomRight}>
|
|
69
|
-
<div class="input-content">
|
|
194
|
+
<div class="input-content" class:multiline class:multiline-expanded={multiline && effectiveRows > 1}>
|
|
70
195
|
{#if icon}
|
|
71
|
-
<
|
|
196
|
+
<div class="icon-wrapper">
|
|
197
|
+
<BaseIcon variant="toned" svg={icon} size={iconSize} />
|
|
198
|
+
</div>
|
|
72
199
|
{/if}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
200
|
+
{#if multiline}
|
|
201
|
+
<!-- svelte-ignore a11y_autofocus -->
|
|
202
|
+
<textarea
|
|
203
|
+
bind:this={inputRef}
|
|
204
|
+
use:ctrlEnterNewline={newlineOnCtrlEnter}
|
|
205
|
+
data-newline-on-ctrl-enter={newlineOnCtrlEnter ? 'true' : undefined}
|
|
206
|
+
{placeholder}
|
|
207
|
+
bind:value
|
|
208
|
+
rows={effectiveRows}
|
|
209
|
+
autofocus={autofocus}
|
|
210
|
+
{disabled}
|
|
211
|
+
style={`resize: ${resize};`}
|
|
212
|
+
on:input
|
|
213
|
+
on:change
|
|
214
|
+
on:focus
|
|
215
|
+
on:blur
|
|
216
|
+
on:keydown
|
|
217
|
+
></textarea>
|
|
218
|
+
{:else}
|
|
219
|
+
<!-- svelte-ignore a11y_autofocus -->
|
|
220
|
+
<input
|
|
221
|
+
bind:this={inputRef}
|
|
222
|
+
{type}
|
|
223
|
+
{placeholder}
|
|
224
|
+
bind:value
|
|
225
|
+
autofocus={autofocus}
|
|
226
|
+
{disabled}
|
|
227
|
+
on:input
|
|
228
|
+
on:change
|
|
229
|
+
on:focus
|
|
230
|
+
on:blur
|
|
231
|
+
on:keydown
|
|
232
|
+
/>
|
|
233
|
+
{/if}
|
|
234
|
+
<div class="icon-wrapper">
|
|
235
|
+
<BaseIcon variant="toned" svg={icons.pen} size="15px" />
|
|
236
|
+
</div>
|
|
86
237
|
</div>
|
|
87
238
|
</BaseContainer>
|
|
88
239
|
</div>
|
|
@@ -92,16 +243,36 @@
|
|
|
92
243
|
width: 100%;
|
|
93
244
|
}
|
|
94
245
|
|
|
246
|
+
.input-wrapper.disabled {
|
|
247
|
+
opacity: 0.7;
|
|
248
|
+
pointer-events: none;
|
|
249
|
+
filter: saturate(0%);
|
|
250
|
+
}
|
|
251
|
+
|
|
95
252
|
.input-content {
|
|
96
253
|
display: flex;
|
|
97
254
|
align-items: center;
|
|
98
255
|
gap: 8px;
|
|
99
|
-
height: 25px;
|
|
256
|
+
min-height: 25px;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.input-content.multiline-expanded {
|
|
260
|
+
align-items: flex-start;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.icon-wrapper {
|
|
264
|
+
flex-shrink: 0;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.input-content.multiline-expanded .icon-wrapper {
|
|
268
|
+
margin-top: 0.2rem;
|
|
100
269
|
}
|
|
101
270
|
|
|
102
271
|
input {
|
|
103
272
|
all: unset;
|
|
273
|
+
display: block;
|
|
104
274
|
flex: 1;
|
|
275
|
+
min-width: 0;
|
|
105
276
|
font-family: 'Noto Serif KR', serif;
|
|
106
277
|
font-weight: 900;
|
|
107
278
|
font-size: 17px;
|
|
@@ -109,7 +280,59 @@
|
|
|
109
280
|
width: 100%;
|
|
110
281
|
}
|
|
111
282
|
|
|
112
|
-
|
|
283
|
+
textarea {
|
|
284
|
+
/* Don't use `all: unset` — Firefox drops intrinsic sizing so `rows` stops working. */
|
|
285
|
+
display: block;
|
|
286
|
+
flex: 1;
|
|
287
|
+
min-width: 0;
|
|
288
|
+
font-family: 'Noto Serif KR', serif;
|
|
289
|
+
font-weight: 900;
|
|
290
|
+
font-size: 17px;
|
|
291
|
+
color: #E3D8D8;
|
|
292
|
+
width: 100%;
|
|
293
|
+
line-height: 1.45;
|
|
294
|
+
min-height: 1.45em;
|
|
295
|
+
white-space: pre-wrap;
|
|
296
|
+
overflow-wrap: break-word;
|
|
297
|
+
margin: 0;
|
|
298
|
+
padding: 0;
|
|
299
|
+
border: none;
|
|
300
|
+
background: transparent;
|
|
301
|
+
outline: none;
|
|
302
|
+
box-shadow: none;
|
|
303
|
+
appearance: none;
|
|
304
|
+
field-sizing: content;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
textarea::-webkit-resizer {
|
|
308
|
+
background: transparent;
|
|
309
|
+
border: none;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
textarea::-webkit-scrollbar-corner {
|
|
313
|
+
background: transparent;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
textarea::-webkit-scrollbar {
|
|
317
|
+
width: 8px;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
textarea::-webkit-scrollbar-track {
|
|
321
|
+
background: rgba(227, 216, 216, 0.05);
|
|
322
|
+
border-radius: 4px;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
textarea::-webkit-scrollbar-thumb {
|
|
326
|
+
background: rgba(227, 216, 216, 0.2);
|
|
327
|
+
border-radius: 4px;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
textarea::-webkit-scrollbar-thumb:hover {
|
|
331
|
+
background: rgba(227, 216, 216, 0.3);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
input::placeholder,
|
|
335
|
+
textarea::placeholder {
|
|
113
336
|
color: rgba(227, 216, 216, 0.4);
|
|
114
337
|
}
|
|
115
338
|
</style>
|