@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 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
- $: renderedContent = markdown && usesContent ? renderMarkdown(content) : '';
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: #FFEFF6;
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
- class="context-menu-wrapper"
98
- style:transform-origin={originMap[origin]}
99
- in:scaleIn
100
- out:scaleOut
101
- >
102
- <BaseContainer theme="filled" padding="0.5rem" borderRadiusTopLeft="28px" borderRadiusTopRight="28px" borderRadiusBottomLeft="28px" borderRadiusBottomRight="28px">
103
- <div style="height: 0.3rem;"></div>
104
- {#each categories as category, catIndex}
105
- {#if category.label}
106
- <div class="separator" class:not-first={catIndex > 0}>
107
- <BaseText textModifier="-4px" variant="button">{category.label}</BaseText>
108
- </div>
109
- {/if}
110
- <div class="category-items">
111
- {#each category.items as {item}}
112
- <button
113
- class="context-item"
114
- class:selected={item.value === selectedValue}
115
- class:disabled={item.disabled}
116
- disabled={item.disabled}
117
- on:click={() => handleSelect(item.value)}
118
- >
119
- {item.label}
120
- </button>
121
- {/each}
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
- {/each}
124
- <div style="height: 0.3rem;"></div>
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
- <BaseIcon variant="toned" svg={icon} size={iconSize} />
196
+ <div class="icon-wrapper">
197
+ <BaseIcon variant="toned" svg={icon} size={iconSize} />
198
+ </div>
72
199
  {/if}
73
- <!-- svelte-ignore a11y_autofocus -->
74
- <input
75
- {type}
76
- {placeholder}
77
- bind:value
78
- autofocus={autofocus}
79
- on:input
80
- on:change
81
- on:focus
82
- on:blur
83
- on:keydown
84
- />
85
- <BaseIcon variant="toned" svg={icons.pen} size="15px" />
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; /* Match button text height */
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
- input::placeholder {
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>