@x33025/sveltely 0.1.18 → 0.1.19

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 (137) hide show
  1. package/dist/actions/LoaderOverlay.svelte +33 -8
  2. package/dist/actions/LoaderOverlay.svelte.d.ts +3 -0
  3. package/dist/actions/loader.d.ts +3 -0
  4. package/dist/actions/loader.js +20 -7
  5. package/dist/components/Library/AnimatedNumber/AnimatedNumber.demo.svelte +3 -9
  6. package/dist/components/Library/ArticleEditor/ArticleEditor.svelte +1 -1
  7. package/dist/components/Library/ArticleEditor/ArticleEditorHeader.svelte +20 -30
  8. package/dist/components/Library/ArticleEditor/Blocks/Code.svelte +0 -1
  9. package/dist/components/Library/ArticleEditor/Blocks/FAQ.svelte +1 -1
  10. package/dist/components/Library/ArticleEditor/Blocks/Heading.svelte +7 -7
  11. package/dist/components/Library/ArticleEditor/Blocks/Image.svelte +20 -36
  12. package/dist/components/Library/ArticleEditor/Blocks/Image.svelte.d.ts +1 -0
  13. package/dist/components/Library/ArticleEditor/Blocks/List.svelte +2 -2
  14. package/dist/components/Library/ArticleEditor/Blocks/Paragraph.svelte +1 -1
  15. package/dist/components/Library/ArticleEditor/Blocks/index.d.ts +0 -1
  16. package/dist/components/Library/ArticleEditor/Blocks/index.js +0 -1
  17. package/dist/components/Library/AsyncButton/AsyncButton.demo.svelte +2 -6
  18. package/dist/components/Library/AsyncButton/AsyncButton.svelte +9 -5
  19. package/dist/components/Library/AsyncButton/AsyncButton.svelte.d.ts +2 -1
  20. package/dist/components/Library/Button/Button.demo.svelte +2 -17
  21. package/dist/components/Library/Button/Button.demo.svelte.d.ts +0 -1
  22. package/dist/components/Library/Button/Button.svelte +15 -16
  23. package/dist/components/Library/Button/Button.svelte.d.ts +2 -1
  24. package/dist/components/Library/Calendar/Calendar.svelte +17 -27
  25. package/dist/components/Library/Checkbox/Checkbox.demo.svelte +7 -4
  26. package/dist/components/Library/Checkbox/Checkbox.svelte +24 -61
  27. package/dist/components/Library/Checkbox/Checkbox.svelte.d.ts +2 -4
  28. package/dist/components/Library/ChipInput/ChipInput.demo.svelte +2 -2
  29. package/dist/components/Library/ChipInput/ChipInput.svelte +7 -11
  30. package/dist/components/Library/ChipInput/ChipInput.svelte.d.ts +3 -2
  31. package/dist/components/Library/Dropdown/Action.svelte +1 -1
  32. package/dist/components/Library/Dropdown/Dropdown.demo.svelte +10 -10
  33. package/dist/components/Library/Dropdown/Dropdown.svelte +2 -6
  34. package/dist/components/Library/Dropdown/Item.svelte +2 -2
  35. package/dist/components/Library/Dropdown/Section.svelte +1 -1
  36. package/dist/components/Library/Dropdown/Trigger.svelte +3 -7
  37. package/dist/components/Library/Image/Image.demo.svelte +3 -3
  38. package/dist/components/Library/Image/Image.svelte +57 -12
  39. package/dist/components/Library/Image/Image.svelte.d.ts +1 -2
  40. package/dist/components/Library/Image/ImagePlaceholder.demo.svelte +12 -0
  41. package/dist/components/Library/Image/ImagePlaceholder.demo.svelte.d.ts +23 -0
  42. package/dist/components/Library/Image/ImagePlaceholder.svelte +28 -4
  43. package/dist/components/Library/Image/ImagePlaceholder.svelte.d.ts +1 -1
  44. package/dist/components/Library/Image/index.d.ts +1 -0
  45. package/dist/components/Library/Image/index.js +1 -0
  46. package/dist/components/Library/ImageMask/BrushPreview.svelte +6 -6
  47. package/dist/components/Library/ImageMask/ImageMask.demo.svelte +10 -8
  48. package/dist/components/Library/ImageMask/ImageMask.svelte +14 -6
  49. package/dist/components/Library/ImageMask/ImageMask.svelte.d.ts +1 -2
  50. package/dist/components/Library/ImageMask/MaskLayer.svelte +12 -6
  51. package/dist/components/Library/Label/Label.demo.svelte +9 -3
  52. package/dist/components/Library/Label/Label.svelte +8 -2
  53. package/dist/components/Library/Link/Link.svelte +10 -22
  54. package/dist/components/Library/Link/Link.svelte.d.ts +2 -3
  55. package/dist/components/Library/Loader/Loader.demo.svelte +9 -3
  56. package/dist/components/Library/NavigationStack/Link.svelte +8 -12
  57. package/dist/components/Library/NavigationStack/Link.svelte.d.ts +1 -3
  58. package/dist/components/Library/NavigationStack/SidebarToggle.svelte +8 -2
  59. package/dist/components/Library/NumberField/NumberField.svelte +21 -17
  60. package/dist/components/Library/NumberField/NumberField.svelte.d.ts +4 -2
  61. package/dist/components/Library/Pagination/Pagination.svelte +3 -3
  62. package/dist/components/Library/Popover/Popover.svelte +2 -7
  63. package/dist/components/Library/ScrollView/ScrollView.demo.svelte +50 -0
  64. package/dist/components/Library/ScrollView/ScrollView.demo.svelte.d.ts +10 -0
  65. package/dist/components/Library/ScrollView/ScrollView.svelte +414 -67
  66. package/dist/components/Library/ScrollView/ScrollView.svelte.d.ts +17 -1
  67. package/dist/components/Library/ScrollView/index.d.ts +1 -1
  68. package/dist/components/Library/SearchField/SearchField.demo.svelte +2 -2
  69. package/dist/components/Library/SearchField/SearchField.svelte +9 -4
  70. package/dist/components/Library/SearchField/SearchField.svelte.d.ts +2 -1
  71. package/dist/components/Library/SegmentedPicker/SegmentedPicker.demo.svelte +2 -2
  72. package/dist/components/Library/SegmentedPicker/SegmentedPicker.svelte +7 -7
  73. package/dist/components/Library/Sheet/Sheet.demo.svelte +1 -1
  74. package/dist/components/Library/Sheet/Sheet.svelte +2 -7
  75. package/dist/components/Library/Slider/Slider.demo.svelte +1 -1
  76. package/dist/components/Library/Slider/Slider.svelte +11 -7
  77. package/dist/components/Library/Slider/Slider.svelte.d.ts +2 -1
  78. package/dist/components/Library/Spinner/Spinner.demo.svelte +1 -1
  79. package/dist/components/Library/Switch/Switch.demo.svelte +7 -4
  80. package/dist/components/Library/Switch/Switch.svelte +28 -68
  81. package/dist/components/Library/Switch/Switch.svelte.d.ts +2 -4
  82. package/dist/components/Library/Table/Column.svelte +81 -0
  83. package/dist/components/Library/Table/Column.svelte.d.ts +39 -0
  84. package/dist/components/Library/Table/Table.demo.svelte +148 -0
  85. package/dist/components/Library/Table/Table.demo.svelte.d.ts +10 -0
  86. package/dist/components/Library/Table/Table.svelte +624 -0
  87. package/dist/components/Library/Table/Table.svelte.d.ts +42 -0
  88. package/dist/components/Library/Table/context.d.ts +5 -0
  89. package/dist/components/Library/Table/context.js +2 -0
  90. package/dist/components/Library/Table/index.js +5 -0
  91. package/dist/components/Library/Table/types.d.ts +37 -0
  92. package/dist/components/Library/Table/types.js +1 -0
  93. package/dist/components/Library/Text/Text.demo.svelte +21 -0
  94. package/dist/components/Library/Text/Text.demo.svelte.d.ts +24 -0
  95. package/dist/components/Library/Text/Text.svelte +41 -0
  96. package/dist/components/Library/Text/Text.svelte.d.ts +9 -0
  97. package/dist/components/Library/Text/index.d.ts +1 -0
  98. package/dist/components/Library/Text/index.js +1 -0
  99. package/dist/components/Library/TextEditor/TextEditor.svelte +15 -9
  100. package/dist/components/Library/TextEditor/TextEditor.svelte.d.ts +2 -4
  101. package/dist/components/Library/TextField/TextField.demo.svelte +1 -1
  102. package/dist/components/Library/TextField/TextField.svelte +21 -18
  103. package/dist/components/Library/TextField/TextField.svelte.d.ts +4 -2
  104. package/dist/components/Library/TextShimmer/TextShimmer.demo.svelte +1 -1
  105. package/dist/components/Library/TimePicker/TimePicker.demo.svelte +10 -2
  106. package/dist/components/Library/TimePicker/TimePicker.svelte +10 -5
  107. package/dist/components/Library/TokenSearchField/TokenSearchField.demo.svelte +4 -2
  108. package/dist/components/Library/TokenSearchField/TokenSearchField.svelte +11 -11
  109. package/dist/components/Library/TokenSearchField/TokenSearchField.svelte.d.ts +2 -1
  110. package/dist/components/Library/WheelPicker/WheelColumn.svelte +183 -126
  111. package/dist/components/Library/WheelPicker/WheelPicker.svelte +4 -4
  112. package/dist/components/Library/WheelPicker/WheelPicker.svelte.d.ts +2 -2
  113. package/dist/components/Library/WheelPicker/index.d.ts +1 -1
  114. package/dist/components/Library/WheelPicker/types.d.ts +6 -0
  115. package/dist/components/Local/ColorStyleControls.svelte +201 -0
  116. package/dist/components/Local/ColorStyleControls.svelte.d.ts +13 -0
  117. package/dist/components/Local/HeroCard.svelte +3 -3
  118. package/dist/components/Local/LayoutStyleControls.svelte +67 -0
  119. package/dist/components/Local/LayoutStyleControls.svelte.d.ts +11 -0
  120. package/dist/components/Local/StyleControls.svelte +48 -124
  121. package/dist/components/Local/StyleControls.svelte.d.ts +7 -5
  122. package/dist/index.d.ts +9 -2
  123. package/dist/index.js +5 -1
  124. package/dist/style/index.css +7 -12
  125. package/dist/style/label.d.ts +2 -1
  126. package/dist/style/label.js +2 -1
  127. package/dist/style/surface.js +4 -0
  128. package/dist/style/text-editor.d.ts +2 -13
  129. package/dist/style/text-editor.js +1 -12
  130. package/dist/style/text.d.ts +26 -0
  131. package/dist/style/text.js +69 -0
  132. package/dist/style/tooltip.d.ts +4 -0
  133. package/dist/style/tooltip.js +1 -0
  134. package/dist/style.css +41 -111
  135. package/package.json +1 -1
  136. package/dist/components/Library/ArticleEditor/Blocks/ImagePreview.svelte +0 -71
  137. package/dist/components/Library/ArticleEditor/Blocks/ImagePreview.svelte.d.ts +0 -8
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { tick } from 'svelte';
2
+ import { tick, untrack } from 'svelte';
3
3
  import type { Snippet } from 'svelte';
4
4
  import { loader as loaderAction } from '../../../actions/loader';
5
5
  import { extractLoaderProps, resolveLoaderOptions, type LoaderProps } from '../../../style/loader';
@@ -33,26 +33,46 @@
33
33
  };
34
34
  };
35
35
 
36
+ export type ScrollViewHandle = {
37
+ element: HTMLElement | null;
38
+ get scrollTop(): number;
39
+ set scrollTop(value: number);
40
+ get scrollLeft(): number;
41
+ set scrollLeft(value: number);
42
+ get clientWidth(): number;
43
+ get clientHeight(): number;
44
+ get scrollWidth(): number;
45
+ get scrollHeight(): number;
46
+ getBoundingClientRect(): DOMRect;
47
+ scrollTo: (options: ScrollToOptions | number, y?: number) => void;
48
+ };
49
+
50
+ export type ScrollViewOverscrollBehavior = 'auto' | 'contain' | 'none';
51
+
36
52
  type Props = {
37
53
  children: Snippet;
38
- viewport?: HTMLElement | null;
54
+ viewport?: ScrollViewHandle | null;
39
55
  axis?: ScrollAxis;
40
56
  contentStyles?: StyleProps;
41
57
  onScroll?: (geometry: ScrollGeometry) => void;
42
58
  scrollGradient?: boolean;
43
59
  scrollGradientSize?: number | string;
60
+ showIndicators?: boolean;
61
+ overscrollBehavior?: ScrollViewOverscrollBehavior;
44
62
  } & LayoutProps &
45
63
  StyleProps &
46
64
  LoaderProps;
47
65
 
48
66
  let {
49
67
  children,
50
- viewport = $bindable<HTMLElement | null>(null),
68
+ viewport = $bindable<ScrollViewHandle | null>(null),
51
69
  axis = 'vertical',
52
70
  contentStyles = {},
53
71
  onScroll,
54
72
  scrollGradient = true,
55
- scrollGradientSize = '1rem',
73
+ scrollGradientSize = '2.5rem',
74
+ showIndicators = true,
75
+ overscrollBehavior = 'contain',
56
76
  ...restProps
57
77
  }: Props = $props();
58
78
 
@@ -76,71 +96,297 @@
76
96
  );
77
97
  let canScrollToTop = $state(false);
78
98
  let canScrollToBottom = $state(false);
99
+ let viewportElement = $state<HTMLDivElement | null>(null);
100
+ let contentElement = $state<HTMLDivElement | null>(null);
101
+ let offsetX = $state(0);
102
+ let offsetY = $state(0);
103
+ let canScrollX = $state(false);
104
+ let canScrollY = $state(false);
105
+ let thumbXSize = $state(0);
106
+ let thumbYSize = $state(0);
107
+ let thumbXOffset = $state(0);
108
+ let thumbYOffset = $state(0);
109
+ let resizeObserver: ResizeObserver | null = null;
110
+ let mutationObserver: MutationObserver | null = null;
79
111
  const scrollGradientEnabled = $derived(scrollGradient && axis !== 'horizontal');
112
+ const verticalScrollbarEnabled = $derived(showIndicators && axis !== 'horizontal' && canScrollY);
113
+ const horizontalScrollbarEnabled = $derived(showIndicators && axis !== 'vertical' && canScrollX);
80
114
 
81
- function syncScrollGradient(geometry: ScrollGeometry) {
82
- if (!scrollGradientEnabled) return;
83
- canScrollToTop = geometry.remaining.top > 0;
84
- canScrollToBottom = geometry.remaining.bottom > 0;
85
- }
115
+ const measuredContentSize = () => {
116
+ if (!contentElement) return { width: 0, height: 0 };
117
+
118
+ const contentRect = contentElement.getBoundingClientRect();
119
+ let width = Math.max(contentElement.scrollWidth, contentElement.offsetWidth, contentRect.width);
120
+ let height = Math.max(contentElement.scrollHeight, contentElement.offsetHeight, contentRect.height);
86
121
 
87
- function getScrollGeometry(node: HTMLElement): ScrollGeometry {
88
- const maxX = Math.max(0, node.scrollWidth - node.clientWidth);
89
- const maxY = Math.max(0, node.scrollHeight - node.clientHeight);
90
- const x = node.scrollLeft;
91
- const y = node.scrollTop;
122
+ for (const child of Array.from(contentElement.children)) {
123
+ const childElement = child as HTMLElement;
124
+ const childRect = childElement.getBoundingClientRect();
125
+ const childRight = childRect.right - contentRect.left;
126
+ const childBottom = childRect.bottom - contentRect.top;
127
+ width = Math.max(width, childElement.scrollWidth, childElement.offsetWidth, childRight);
128
+ height = Math.max(height, childElement.scrollHeight, childElement.offsetHeight, childBottom);
129
+ }
130
+
131
+ return { width, height };
132
+ };
133
+ const contentWidth = () => measuredContentSize().width;
134
+ const contentHeight = () => measuredContentSize().height;
135
+ const maxOffsetX = () =>
136
+ viewportElement ? Math.max(0, contentWidth() - viewportElement.clientWidth) : 0;
137
+ const maxOffsetY = () =>
138
+ viewportElement ? Math.max(0, contentHeight() - viewportElement.clientHeight) : 0;
139
+ const clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max);
140
+
141
+ function getScrollGeometry(): ScrollGeometry {
142
+ const maxX = maxOffsetX();
143
+ const maxY = maxOffsetY();
92
144
 
93
145
  return {
94
146
  axis,
95
- offset: { x, y },
147
+ offset: { x: offsetX, y: offsetY },
96
148
  viewport: {
97
- width: node.clientWidth,
98
- height: node.clientHeight
149
+ width: viewportElement?.clientWidth ?? 0,
150
+ height: viewportElement?.clientHeight ?? 0
99
151
  },
100
152
  content: {
101
- width: node.scrollWidth,
102
- height: node.scrollHeight
153
+ width: contentWidth(),
154
+ height: contentHeight()
103
155
  },
104
156
  remaining: {
105
- top: y,
106
- right: maxX - x,
107
- bottom: maxY - y,
108
- left: x
157
+ top: offsetY,
158
+ right: maxX - offsetX,
159
+ bottom: maxY - offsetY,
160
+ left: offsetX
109
161
  },
110
162
  progress: {
111
- x: maxX === 0 ? 1 : x / maxX,
112
- y: maxY === 0 ? 1 : y / maxY
163
+ x: maxX === 0 ? 1 : offsetX / maxX,
164
+ y: maxY === 0 ? 1 : offsetY / maxY
113
165
  }
114
166
  };
115
167
  }
116
168
 
117
- function handleScroll(event: Event) {
118
- const geometry = getScrollGeometry(event.currentTarget as HTMLElement);
119
- syncScrollGradient(geometry);
120
- onScroll?.(geometry);
169
+ function syncScrollbarGeometry() {
170
+ if (!viewportElement || !contentElement) {
171
+ canScrollX = false;
172
+ canScrollY = false;
173
+ thumbXSize = 0;
174
+ thumbYSize = 0;
175
+ thumbXOffset = 0;
176
+ thumbYOffset = 0;
177
+ return;
178
+ }
179
+ const maxX = maxOffsetX();
180
+ const maxY = maxOffsetY();
181
+ canScrollX = maxX > 1;
182
+ canScrollY = maxY > 1;
183
+ offsetX = clamp(offsetX, 0, maxX);
184
+ offsetY = clamp(offsetY, 0, maxY);
185
+
186
+ if (canScrollX) {
187
+ const ratio = viewportElement.clientWidth / contentWidth();
188
+ thumbXSize = Math.max(24, viewportElement.clientWidth * ratio);
189
+ thumbXOffset = (offsetX / maxX) * Math.max(0, viewportElement.clientWidth - thumbXSize);
190
+ } else {
191
+ thumbXSize = 0;
192
+ thumbXOffset = 0;
193
+ }
194
+
195
+ if (canScrollY) {
196
+ const ratio = viewportElement.clientHeight / contentHeight();
197
+ thumbYSize = Math.max(24, viewportElement.clientHeight * ratio);
198
+ thumbYOffset = (offsetY / maxY) * Math.max(0, viewportElement.clientHeight - thumbYSize);
199
+ } else {
200
+ thumbYSize = 0;
201
+ thumbYOffset = 0;
202
+ }
121
203
  }
122
204
 
205
+ function syncScrollGradient() {
206
+ if (!scrollGradientEnabled || !viewportElement || !contentElement) {
207
+ canScrollToTop = false;
208
+ canScrollToBottom = false;
209
+ return;
210
+ }
211
+
212
+ const maxY = maxOffsetY();
213
+ canScrollToTop = maxY > 1 && offsetY > 1;
214
+ canScrollToBottom = maxY > 1 && maxY - offsetY > 1;
215
+ }
216
+
217
+ function publishScroll() {
218
+ syncScrollbarGeometry();
219
+ syncScrollGradient();
220
+ onScroll?.(getScrollGeometry());
221
+ }
222
+
223
+ function scrollTo(nextX: number, nextY: number) {
224
+ const previousX = offsetX;
225
+ const previousY = offsetY;
226
+ offsetX = clamp(axis === 'vertical' ? 0 : nextX, 0, maxOffsetX());
227
+ offsetY = clamp(axis === 'horizontal' ? 0 : nextY, 0, maxOffsetY());
228
+ publishScroll();
229
+ return offsetX !== previousX || offsetY !== previousY;
230
+ }
231
+
232
+ function shouldContainOverscroll(consumed: boolean) {
233
+ if (overscrollBehavior === 'none') return true;
234
+ return overscrollBehavior === 'contain' && consumed;
235
+ }
236
+
237
+ const scrollViewHandle: ScrollViewHandle = {
238
+ get element() {
239
+ return viewportElement;
240
+ },
241
+ get scrollTop() {
242
+ return offsetY;
243
+ },
244
+ set scrollTop(value: number) {
245
+ scrollTo(offsetX, value);
246
+ },
247
+ get scrollLeft() {
248
+ return offsetX;
249
+ },
250
+ set scrollLeft(value: number) {
251
+ scrollTo(value, offsetY);
252
+ },
253
+ get clientWidth() {
254
+ return viewportElement?.clientWidth ?? 0;
255
+ },
256
+ get clientHeight() {
257
+ return viewportElement?.clientHeight ?? 0;
258
+ },
259
+ get scrollWidth() {
260
+ return contentWidth();
261
+ },
262
+ get scrollHeight() {
263
+ return contentHeight();
264
+ },
265
+ getBoundingClientRect() {
266
+ return viewportElement?.getBoundingClientRect() ?? new DOMRect();
267
+ },
268
+ scrollTo(options: ScrollToOptions | number, y?: number) {
269
+ if (typeof options === 'number') {
270
+ scrollTo(options, y ?? offsetY);
271
+ return;
272
+ }
273
+ scrollTo(options.left ?? offsetX, options.top ?? offsetY);
274
+ }
275
+ };
276
+
123
277
  function handleWheel(event: WheelEvent) {
124
- const node = event.currentTarget as HTMLElement;
278
+ const wheelX = axis === 'horizontal' && event.deltaX === 0 ? event.deltaY : event.deltaX;
279
+ const consumed = scrollTo(offsetX + wheelX, offsetY + event.deltaY);
125
280
 
126
- event.preventDefault();
127
- event.stopPropagation();
281
+ if (consumed || shouldContainOverscroll(consumed)) {
282
+ event.preventDefault();
283
+ event.stopPropagation();
284
+ }
285
+ }
286
+
287
+ function handleKeydown(event: KeyboardEvent) {
288
+ const line = 40;
289
+ const pageX = viewportElement?.clientWidth ?? 0;
290
+ const pageY = viewportElement?.clientHeight ?? 0;
291
+
292
+ let consumed = false;
128
293
 
129
- if (axis !== 'horizontal') node.scrollTop += event.deltaY;
130
- if (axis !== 'vertical') node.scrollLeft += event.deltaX;
294
+ if (event.key === 'ArrowDown') consumed = scrollTo(offsetX, offsetY + line);
295
+ else if (event.key === 'ArrowUp') consumed = scrollTo(offsetX, offsetY - line);
296
+ else if (event.key === 'ArrowRight') consumed = scrollTo(offsetX + line, offsetY);
297
+ else if (event.key === 'ArrowLeft') consumed = scrollTo(offsetX - line, offsetY);
298
+ else if (event.key === 'PageDown') consumed = scrollTo(offsetX, offsetY + pageY);
299
+ else if (event.key === 'PageUp') consumed = scrollTo(offsetX, offsetY - pageY);
300
+ else if (event.key === 'Home') consumed = scrollTo(0, 0);
301
+ else if (event.key === 'End') consumed = scrollTo(maxOffsetX(), maxOffsetY());
302
+ else return;
303
+
304
+ if (consumed || shouldContainOverscroll(consumed)) event.preventDefault();
131
305
  }
132
306
 
133
307
  $effect(() => {
134
- if (!scrollGradientEnabled || !viewport) return;
308
+ if (!viewportElement || !contentElement) return;
309
+ viewport = scrollViewHandle;
310
+ resizeObserver?.disconnect();
311
+ resizeObserver = new ResizeObserver(() => untrack(publishScroll));
312
+ resizeObserver.observe(viewportElement);
313
+ resizeObserver.observe(contentElement);
314
+
315
+ const observeContentChildren = () => {
316
+ if (!resizeObserver || !contentElement) return;
317
+ for (const child of Array.from(contentElement.children)) {
318
+ resizeObserver.observe(child);
319
+ }
320
+ };
321
+
322
+ observeContentChildren();
323
+ mutationObserver?.disconnect();
324
+ mutationObserver = new MutationObserver(() => {
325
+ observeContentChildren();
326
+ untrack(publishScroll);
327
+ });
328
+ mutationObserver.observe(contentElement, { childList: true });
329
+
135
330
  void tick().then(() => {
136
- if (!viewport) return;
137
- syncScrollGradient(getScrollGeometry(viewport));
331
+ if (!viewportElement || !contentElement) return;
332
+ untrack(publishScroll);
138
333
  });
334
+
335
+ return () => {
336
+ resizeObserver?.disconnect();
337
+ resizeObserver = null;
338
+ mutationObserver?.disconnect();
339
+ mutationObserver = null;
340
+ };
341
+ });
342
+
343
+ $effect(() => {
344
+ scrollGradientEnabled;
345
+ if (!viewportElement || !contentElement) return;
346
+ untrack(publishScroll);
139
347
  });
348
+
349
+ function startThumbDrag(event: PointerEvent, orientation: 'horizontal' | 'vertical') {
350
+ if (!viewportElement) return;
351
+
352
+ event.preventDefault();
353
+ event.stopPropagation();
354
+
355
+ const startPointer = orientation === 'horizontal' ? event.clientX : event.clientY;
356
+ const startScroll = orientation === 'horizontal' ? offsetX : offsetY;
357
+
358
+ const move = (moveEvent: PointerEvent) => {
359
+ if (!viewportElement) return;
360
+ const viewportSize =
361
+ orientation === 'horizontal' ? viewportElement.clientWidth : viewportElement.clientHeight;
362
+ const contentSize = orientation === 'horizontal' ? contentWidth() : contentHeight();
363
+ const thumbSize = orientation === 'horizontal' ? thumbXSize : thumbYSize;
364
+ const maxScroll = Math.max(0, contentSize - viewportSize);
365
+ const maxThumbOffset = Math.max(1, viewportSize - thumbSize);
366
+ const delta = (orientation === 'horizontal' ? moveEvent.clientX : moveEvent.clientY) - startPointer;
367
+ const nextScroll = startScroll + (delta / maxThumbOffset) * maxScroll;
368
+
369
+ if (orientation === 'horizontal') {
370
+ scrollTo(nextScroll, offsetY);
371
+ } else {
372
+ scrollTo(offsetX, nextScroll);
373
+ }
374
+ };
375
+
376
+ const up = () => {
377
+ window.removeEventListener('pointermove', move);
378
+ window.removeEventListener('pointerup', up);
379
+ window.removeEventListener('pointercancel', up);
380
+ };
381
+
382
+ window.addEventListener('pointermove', move);
383
+ window.addEventListener('pointerup', up);
384
+ window.addEventListener('pointercancel', up);
385
+ }
386
+
140
387
  </script>
141
388
 
142
389
  <div
143
- bind:this={viewport}
144
390
  class="scroll-view"
145
391
  class:scroll-view-vertical={axis === 'vertical'}
146
392
  class:scroll-view-horizontal={axis === 'horizontal'}
@@ -151,17 +397,56 @@
151
397
  style={rootStyle}
152
398
  {...props}
153
399
  use:loaderAction={loaderOptions}
154
- onscroll={handleScroll}
155
- onwheel={handleWheel}
156
400
  >
157
401
  {#if scrollGradientEnabled}
158
- <div class="scroll-view-gradient scroll-view-gradient-top"></div>
402
+ <div class="scroll-view-gradient scroll-view-gradient-top" aria-hidden="true"></div>
159
403
  {/if}
160
- <div class="scroll-view-content" style={contentStyle}>
161
- {@render children()}
404
+ <!-- svelte-ignore a11y_no_noninteractive_tabindex, a11y_no_noninteractive_element_interactions -->
405
+ <div
406
+ bind:this={viewportElement}
407
+ class="scroll-view-viewport"
408
+ role="region"
409
+ aria-label="Scrollable content"
410
+ tabindex="0"
411
+ onwheel={handleWheel}
412
+ onkeydown={handleKeydown}
413
+ >
414
+ <div
415
+ bind:this={contentElement}
416
+ class="scroll-view-content"
417
+ style={`${contentStyle} transform: translate3d(${-offsetX}px, ${-offsetY}px, 0);`}
418
+ >
419
+ {@render children()}
420
+ </div>
162
421
  </div>
163
422
  {#if scrollGradientEnabled}
164
- <div class="scroll-view-gradient scroll-view-gradient-bottom"></div>
423
+ <div class="scroll-view-gradient scroll-view-gradient-bottom" aria-hidden="true"></div>
424
+ {/if}
425
+ {#if verticalScrollbarEnabled}
426
+ <div class="scroll-view-scrollbar scroll-view-scrollbar-vertical" aria-hidden="true">
427
+ <button
428
+ type="button"
429
+ class="scroll-view-thumb scroll-view-thumb-vertical"
430
+ aria-label="Scroll vertically"
431
+ style:height={`${thumbYSize}px`}
432
+ style:transform={`translateY(${thumbYOffset}px)`}
433
+ onpointerdown={(event) => startThumbDrag(event, 'vertical')}
434
+ tabindex="-1"
435
+ ></button>
436
+ </div>
437
+ {/if}
438
+ {#if horizontalScrollbarEnabled}
439
+ <div class="scroll-view-scrollbar scroll-view-scrollbar-horizontal" aria-hidden="true">
440
+ <button
441
+ type="button"
442
+ class="scroll-view-thumb scroll-view-thumb-horizontal"
443
+ aria-label="Scroll horizontally"
444
+ style:width={`${thumbXSize}px`}
445
+ style:transform={`translateX(${thumbXOffset}px)`}
446
+ onpointerdown={(event) => startThumbDrag(event, 'horizontal')}
447
+ tabindex="-1"
448
+ ></button>
449
+ </div>
165
450
  {/if}
166
451
  </div>
167
452
 
@@ -171,33 +456,29 @@
171
456
  min-height: 0;
172
457
  width: 100%;
173
458
  height: 100%;
174
- border-radius: var(--scroll-view-border-radius, var(--sveltely-border-radius));
459
+ border-radius: var(--scroll-view-border-radius, 0);
175
460
  background: var(--scroll-view-background, transparent);
176
461
  color: var(--scroll-view-color, inherit);
177
- scrollbar-gutter: stable;
178
- overscroll-behavior: contain;
179
462
  position: relative;
463
+ overflow: hidden;
180
464
  }
181
465
 
182
- .scroll-view-vertical {
183
- overflow-x: hidden;
184
- overflow-y: auto;
185
- }
186
-
187
- .scroll-view-horizontal {
188
- overflow-x: auto;
189
- overflow-y: hidden;
190
- }
191
-
192
- .scroll-view-both {
193
- overflow: auto;
466
+ .scroll-view-viewport {
467
+ width: 100%;
468
+ height: 100%;
469
+ min-width: 0;
470
+ min-height: 0;
471
+ overflow: hidden;
472
+ touch-action: none;
194
473
  }
195
474
 
196
475
  .scroll-view-content {
476
+ width: 100%;
197
477
  min-width: 100%;
198
478
  min-height: 100%;
199
479
  padding: var(--scroll-view-content-padding-y, var(--sveltely-inset))
200
480
  var(--scroll-view-content-padding-x, var(--sveltely-inset));
481
+ will-change: transform;
201
482
  }
202
483
 
203
484
  .scroll-view-horizontal .scroll-view-content {
@@ -215,10 +496,11 @@
215
496
  }
216
497
 
217
498
  .scroll-view-gradient {
218
- position: sticky;
499
+ position: absolute;
500
+ left: 0;
501
+ right: 0;
219
502
  z-index: 2;
220
503
  height: var(--scroll-view-gradient-size, 1rem);
221
- flex: 0 0 var(--scroll-view-gradient-size, 1rem);
222
504
  pointer-events: none;
223
505
  opacity: 0;
224
506
  transition: opacity 120ms ease;
@@ -226,18 +508,83 @@
226
508
 
227
509
  .scroll-view-gradient-top {
228
510
  top: 0;
229
- margin-bottom: calc(var(--scroll-view-gradient-size, 1rem) * -1);
230
- background: linear-gradient(to bottom, var(--scroll-view-background, white), transparent);
511
+ background: linear-gradient(
512
+ 180deg in oklab,
513
+ rgb(255 255 255 / 0.72) 0%,
514
+ rgb(255 255 255 / 0.67) 8.1%,
515
+ rgb(255 255 255 / 0.58) 15.5%,
516
+ rgb(255 255 255 / 0.46) 24.5%,
517
+ rgb(255 255 255 / 0.32) 38.4%,
518
+ rgb(255 255 255 / 0.18) 58.7%,
519
+ rgb(255 255 255 / 0.07) 80.2%,
520
+ rgb(255 255 255 / 0) 100%
521
+ );
231
522
  }
232
523
 
233
524
  .scroll-view-gradient-bottom {
234
525
  bottom: 0;
235
- margin-top: calc(var(--scroll-view-gradient-size, 1rem) * -1);
236
- background: linear-gradient(to top, var(--scroll-view-background, white), transparent);
526
+ background: linear-gradient(
527
+ 0deg in oklab,
528
+ rgb(255 255 255 / 0.72) 0%,
529
+ rgb(255 255 255 / 0.67) 8.1%,
530
+ rgb(255 255 255 / 0.58) 15.5%,
531
+ rgb(255 255 255 / 0.46) 24.5%,
532
+ rgb(255 255 255 / 0.32) 38.4%,
533
+ rgb(255 255 255 / 0.18) 58.7%,
534
+ rgb(255 255 255 / 0.07) 80.2%,
535
+ rgb(255 255 255 / 0) 100%
536
+ );
237
537
  }
238
538
 
239
- .scroll-view-can-scroll-up .scroll-view-gradient-top,
240
- .scroll-view-can-scroll-down .scroll-view-gradient-bottom {
539
+ .scroll-view-can-scroll-up > .scroll-view-gradient-top,
540
+ .scroll-view-can-scroll-down > .scroll-view-gradient-bottom {
241
541
  opacity: 1;
242
542
  }
543
+
544
+ .scroll-view-scrollbar {
545
+ position: absolute;
546
+ z-index: 3;
547
+ pointer-events: none;
548
+ }
549
+
550
+ .scroll-view-scrollbar-vertical {
551
+ top: calc(var(--sveltely-inset) * 0.5);
552
+ right: calc(var(--sveltely-inset) * 0.5);
553
+ bottom: calc(var(--sveltely-inset) * 0.5);
554
+ width: 0.375rem;
555
+ }
556
+
557
+ .scroll-view-scrollbar-horizontal {
558
+ right: calc(var(--sveltely-inset) * 0.5);
559
+ bottom: calc(var(--sveltely-inset) * 0.5);
560
+ left: calc(var(--sveltely-inset) * 0.5);
561
+ height: 0.375rem;
562
+ }
563
+
564
+ .scroll-view-thumb {
565
+ position: absolute;
566
+ display: block;
567
+ border: 0;
568
+ border-radius: 999px;
569
+ background: color-mix(in oklab, var(--sveltely-text-secondary-color) 48%, transparent);
570
+ padding: 0;
571
+ pointer-events: auto;
572
+ transition: background-color 120ms ease;
573
+ }
574
+
575
+ .scroll-view-thumb:hover {
576
+ background: color-mix(in oklab, var(--sveltely-text-secondary-color) 72%, transparent);
577
+ }
578
+
579
+ .scroll-view-thumb-vertical {
580
+ top: 0;
581
+ right: 0;
582
+ width: 100%;
583
+ }
584
+
585
+ .scroll-view-thumb-horizontal {
586
+ bottom: 0;
587
+ left: 0;
588
+ height: 100%;
589
+ }
243
590
  </style>
@@ -28,14 +28,30 @@ export type ScrollGeometry = {
28
28
  y: number;
29
29
  };
30
30
  };
31
+ export type ScrollViewHandle = {
32
+ element: HTMLElement | null;
33
+ get scrollTop(): number;
34
+ set scrollTop(value: number);
35
+ get scrollLeft(): number;
36
+ set scrollLeft(value: number);
37
+ get clientWidth(): number;
38
+ get clientHeight(): number;
39
+ get scrollWidth(): number;
40
+ get scrollHeight(): number;
41
+ getBoundingClientRect(): DOMRect;
42
+ scrollTo: (options: ScrollToOptions | number, y?: number) => void;
43
+ };
44
+ export type ScrollViewOverscrollBehavior = 'auto' | 'contain' | 'none';
31
45
  type Props = {
32
46
  children: Snippet;
33
- viewport?: HTMLElement | null;
47
+ viewport?: ScrollViewHandle | null;
34
48
  axis?: ScrollAxis;
35
49
  contentStyles?: StyleProps;
36
50
  onScroll?: (geometry: ScrollGeometry) => void;
37
51
  scrollGradient?: boolean;
38
52
  scrollGradientSize?: number | string;
53
+ showIndicators?: boolean;
54
+ overscrollBehavior?: ScrollViewOverscrollBehavior;
39
55
  } & LayoutProps & StyleProps & LoaderProps;
40
56
  declare const ScrollView: import("svelte").Component<Props, {}, "viewport">;
41
57
  type ScrollView = ReturnType<typeof ScrollView>;
@@ -1,2 +1,2 @@
1
1
  export { default } from './ScrollView.svelte';
2
- export type { ScrollGeometry } from './ScrollView.svelte';
2
+ export type { ScrollGeometry, ScrollViewHandle, ScrollViewOverscrollBehavior } from './ScrollView.svelte';
@@ -1,6 +1,6 @@
1
1
  <script module lang="ts">
2
2
  export const demo = {
3
- name: 'SearchField',
3
+ name: 'Search Field',
4
4
  description: 'Search field with icon, input, and optional confirmation hint.'
5
5
  };
6
6
  </script>
@@ -13,5 +13,5 @@
13
13
 
14
14
  <div class="vstack w-full max-w-sm gap-2">
15
15
  <SearchField bind:value placeholder="Search components..." />
16
- <p class="text-xs text-[var(--sveltely-secondary-color)]">Value: {value || 'empty'}</p>
16
+ <p class="text-xs text-[var(--sveltely-text-secondary-color)]">Value: {value || 'empty'}</p>
17
17
  </div>