@x33025/sveltely 0.1.23 → 0.1.25

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 (107) hide show
  1. package/dist/actions/LoaderOverlay.svelte +10 -3
  2. package/dist/components/Library/Accordion/Accordion.demo.svelte +21 -0
  3. package/dist/components/Library/Accordion/Accordion.demo.svelte.d.ts +9 -0
  4. package/dist/components/Library/Accordion/Accordion.svelte +78 -0
  5. package/dist/components/Library/Accordion/Accordion.svelte.d.ts +14 -0
  6. package/dist/components/Library/Accordion/Content.svelte +57 -0
  7. package/dist/components/Library/Accordion/Content.svelte.d.ts +8 -0
  8. package/dist/components/Library/Accordion/Header.svelte +98 -0
  9. package/dist/components/Library/Accordion/Header.svelte.d.ts +10 -0
  10. package/dist/components/Library/Accordion/context.d.ts +9 -0
  11. package/dist/components/Library/Accordion/context.js +6 -0
  12. package/dist/components/Library/Accordion/index.d.ts +9 -0
  13. package/dist/components/Library/Accordion/index.js +7 -0
  14. package/dist/components/Library/AnimatedNumber/AnimatedNumber.demo.svelte +3 -2
  15. package/dist/components/Library/ArticleEditor/ArticleEditor.svelte +1 -2
  16. package/dist/components/Library/ArticleEditor/Blocks/Table.svelte +133 -172
  17. package/dist/components/Library/Checkbox/Checkbox.demo.svelte +5 -4
  18. package/dist/components/Library/Checkbox/Checkbox.svelte +6 -5
  19. package/dist/components/Library/Checkbox/Checkbox.svelte.d.ts +2 -1
  20. package/dist/components/Library/ChipInput/ChipInput.demo.svelte +3 -2
  21. package/dist/components/Library/Dropdown/Dropdown.demo.svelte +20 -15
  22. package/dist/components/Library/Floating/Floating.svelte +5 -6
  23. package/dist/components/Library/Grid/Grid.demo.svelte +58 -0
  24. package/dist/components/Library/Grid/Grid.demo.svelte.d.ts +25 -0
  25. package/dist/components/Library/Grid/Grid.svelte +128 -25
  26. package/dist/components/Library/Grid/Grid.svelte.d.ts +38 -9
  27. package/dist/components/Library/Grid/GridItem.svelte +18 -14
  28. package/dist/components/Library/Grid/GridItem.svelte.d.ts +2 -1
  29. package/dist/components/Library/HStack/HStack.svelte +4 -4
  30. package/dist/components/Library/HStack/HStack.svelte.d.ts +2 -1
  31. package/dist/components/Library/Image/Image.demo.svelte +3 -1
  32. package/dist/components/Library/Image/Image.demo.svelte.d.ts +2 -0
  33. package/dist/components/Library/ImageMask/ImageMask.demo.svelte +8 -6
  34. package/dist/components/Library/Label/Label.demo.svelte +5 -5
  35. package/dist/components/Library/Label/Label.svelte +10 -26
  36. package/dist/components/Library/NavigationStack/Link.svelte +1 -4
  37. package/dist/components/Library/Notifications/Notifications.demo.svelte +63 -0
  38. package/dist/components/Library/Notifications/Notifications.demo.svelte.d.ts +9 -0
  39. package/dist/components/Library/Notifications/Notifications.svelte +155 -0
  40. package/dist/components/Library/Notifications/Notifications.svelte.d.ts +35 -0
  41. package/dist/components/Library/Notifications/index.d.ts +2 -0
  42. package/dist/components/Library/Notifications/index.js +1 -0
  43. package/dist/components/Library/Notifications/types.d.ts +8 -0
  44. package/dist/components/Library/Notifications/types.js +1 -0
  45. package/dist/components/Library/NumberField/NumberField.svelte +25 -19
  46. package/dist/components/Library/Pagination/Pagination.demo.svelte +3 -2
  47. package/dist/components/Library/Pagination/Pagination.svelte +6 -18
  48. package/dist/components/Library/Popover/PopoverDebugOverlay.svelte +3 -3
  49. package/dist/components/Library/Portal/Content.svelte +20 -0
  50. package/dist/components/Library/Portal/Content.svelte.d.ts +10 -0
  51. package/dist/components/Library/Portal/Portal.svelte +4 -0
  52. package/dist/components/Library/Portal/Portal.svelte.d.ts +1 -0
  53. package/dist/components/Library/Portal/index.d.ts +1 -0
  54. package/dist/components/Library/Portal/index.js +1 -0
  55. package/dist/components/Library/ScrollView/ScrollView.svelte +88 -9
  56. package/dist/components/Library/ScrollView/ScrollView.svelte.d.ts +9 -2
  57. package/dist/components/Library/ScrollView/index.d.ts +1 -1
  58. package/dist/components/Library/SearchField/SearchField.demo.svelte +3 -2
  59. package/dist/components/Library/SearchField/SearchField.svelte +5 -5
  60. package/dist/components/Library/SearchField/SearchField.svelte.d.ts +2 -1
  61. package/dist/components/Library/SegmentedPicker/SegmentedPicker.demo.svelte +3 -2
  62. package/dist/components/Library/Sheet/Sheet.demo.svelte +3 -2
  63. package/dist/components/Library/Sheet/Sheet.svelte +3 -3
  64. package/dist/components/Library/Slider/Slider.demo.svelte +3 -2
  65. package/dist/components/Library/Spinner/Spinner.demo.svelte +3 -2
  66. package/dist/components/Library/Switch/Switch.demo.svelte +5 -4
  67. package/dist/components/Library/Switch/Switch.svelte +6 -5
  68. package/dist/components/Library/Switch/Switch.svelte.d.ts +2 -1
  69. package/dist/components/Library/Table/Column.svelte +3 -0
  70. package/dist/components/Library/Table/Column.svelte.d.ts +1 -0
  71. package/dist/components/Library/Table/Table.demo.svelte +230 -17
  72. package/dist/components/Library/Table/Table.svelte +322 -78
  73. package/dist/components/Library/Table/Table.svelte.d.ts +5 -0
  74. package/dist/components/Library/Table/types.d.ts +1 -0
  75. package/dist/components/Library/TextField/TextField.svelte +20 -14
  76. package/dist/components/Library/TextShimmer/TextShimmer.demo.svelte +3 -2
  77. package/dist/components/Library/TimePicker/TimePicker.demo.svelte +3 -10
  78. package/dist/components/Library/TokenSearchField/TokenSearchField.demo.svelte +3 -2
  79. package/dist/components/Library/VStack/VStack.svelte +4 -4
  80. package/dist/components/Library/VStack/VStack.svelte.d.ts +2 -1
  81. package/dist/components/Local/ColorStyleControls.svelte +25 -72
  82. package/dist/components/Local/ComponentGrid.svelte +99 -27
  83. package/dist/components/Local/ComponentGrid.svelte.d.ts +2 -1
  84. package/dist/components/Local/ComponentPage.svelte +74 -0
  85. package/dist/components/Local/ComponentPage.svelte.d.ts +13 -0
  86. package/dist/components/Local/HeroCard.svelte +10 -6
  87. package/dist/components/Local/LayoutStyleControls.svelte +33 -25
  88. package/dist/components/Local/StyleControls.svelte +1 -1
  89. package/dist/index.d.ts +8 -3
  90. package/dist/index.js +4 -1
  91. package/dist/style/index.css +3 -4
  92. package/dist/style/layout.d.ts +15 -36
  93. package/dist/style/layout.js +35 -83
  94. package/dist/style/surface.d.ts +1 -0
  95. package/dist/style/surface.js +10 -0
  96. package/dist/style.css +3 -51
  97. package/dist/viewport/geometry.d.ts +8 -0
  98. package/dist/viewport/geometry.js +43 -0
  99. package/dist/viewport/index.d.ts +4 -0
  100. package/dist/viewport/index.js +4 -0
  101. package/dist/viewport/layout.d.ts +4 -0
  102. package/dist/viewport/layout.js +138 -0
  103. package/dist/viewport/placement.d.ts +4 -0
  104. package/dist/viewport/placement.js +14 -0
  105. package/dist/viewport/types.d.ts +81 -0
  106. package/dist/viewport/types.js +1 -0
  107. package/package.json +1 -1
@@ -0,0 +1,155 @@
1
+ <script lang="ts" generics="TItem extends NotificationItem = NotificationItem">
2
+ import type { Snippet } from 'svelte';
3
+ import { flip } from 'svelte/animate';
4
+ import type { HTMLAttributes } from 'svelte/elements';
5
+ import { fade, fly } from 'svelte/transition';
6
+ import { extractLayoutProps, layoutStyle, type LayoutProps } from '../../../style/layout';
7
+ import { extractStyleProps, surfaceStyle, type StyleProps } from '../../../style/surface';
8
+ import type { NotificationItem } from './types';
9
+
10
+ type Props = {
11
+ items?: TItem[];
12
+ children?: Snippet<[TItem, number]>;
13
+ empty?: Snippet;
14
+ key?: (item: TItem, index: number) => string | number;
15
+ animationDuration?: number;
16
+ } & LayoutProps &
17
+ StyleProps &
18
+ Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'class' | 'style'>;
19
+
20
+ let {
21
+ items = $bindable<TItem[]>([]),
22
+ children,
23
+ empty,
24
+ key,
25
+ animationDuration = 180,
26
+ ...restProps
27
+ }: Props = $props();
28
+
29
+ const extractedLayoutProps = $derived.by(() => extractLayoutProps(restProps));
30
+ const layoutProps = $derived(extractedLayoutProps.layoutProps);
31
+ const afterLayoutProps = $derived(extractedLayoutProps.restProps);
32
+ const extractedStyleProps = $derived.by(() => extractStyleProps(afterLayoutProps));
33
+ const styleProps = $derived(extractedStyleProps.styleProps);
34
+ const forwardedProps = $derived(extractedStyleProps.restProps);
35
+ const rootStyle = $derived.by(() =>
36
+ [layoutStyle(layoutProps), surfaceStyle(styleProps, 'notifications')].filter(Boolean).join(' ')
37
+ );
38
+
39
+ function itemKey(item: TItem, index: number) {
40
+ return key?.(item, index) ?? item.id ?? index;
41
+ }
42
+
43
+ function itemTitle(item: TItem) {
44
+ return item.title ?? String(item.id ?? 'Notification');
45
+ }
46
+
47
+ function itemMessage(item: TItem) {
48
+ return item.message ?? item.description ?? '';
49
+ }
50
+ </script>
51
+
52
+ <div class="notifications" style={rootStyle} {...forwardedProps}>
53
+ {#each items as item, index (itemKey(item, index))}
54
+ <div
55
+ class="notification"
56
+ data-tone={item.tone ?? 'default'}
57
+ in:fly={{ y: -8, duration: animationDuration }}
58
+ out:fade={{ duration: animationDuration * 0.75 }}
59
+ animate:flip={{ duration: animationDuration }}
60
+ >
61
+ {#if children}
62
+ {@render children(item, index)}
63
+ {:else}
64
+ <p class="notification-title">{itemTitle(item)}</p>
65
+ {#if itemMessage(item)}
66
+ <p class="notification-message">{itemMessage(item)}</p>
67
+ {/if}
68
+ {/if}
69
+ </div>
70
+ {:else}
71
+ {#if empty}
72
+ <div class="notifications-empty">
73
+ {@render empty()}
74
+ </div>
75
+ {/if}
76
+ {/each}
77
+ </div>
78
+
79
+ <style>
80
+ .notifications {
81
+ --notifications-font-size: var(--sveltely-font-size);
82
+ --notifications-padding-x: 0px;
83
+ --notifications-padding-y: 0px;
84
+ --notifications-gap: var(--sveltely-gap);
85
+ --notifications-border-radius: var(--sveltely-border-radius);
86
+ display: inline-flex;
87
+ min-width: 0;
88
+ min-height: 0;
89
+ flex-direction: column;
90
+ align-items: stretch;
91
+ gap: var(--notifications-gap);
92
+ border-radius: var(--notifications-border-radius);
93
+ padding: var(--notifications-padding-y) var(--notifications-padding-x);
94
+ font-size: var(--notifications-font-size);
95
+ }
96
+
97
+ .notification {
98
+ position: relative;
99
+ min-width: min(100%, 16rem);
100
+ border: 1px solid var(--sveltely-border-color);
101
+ border-radius: var(--sveltely-border-radius);
102
+ background: var(--sveltely-background-color);
103
+ box-shadow: var(--sveltely-shadow);
104
+ color: var(--sveltely-text-primary-color);
105
+ padding: calc(var(--sveltely-padding-y) * 0.83) var(--sveltely-padding-x);
106
+ overflow: hidden;
107
+ }
108
+
109
+ .notification::before {
110
+ position: absolute;
111
+ inset-block: 0;
112
+ inset-inline-start: 0;
113
+ width: 3px;
114
+ background: var(--notification-accent, var(--sveltely-control-active-color));
115
+ content: '';
116
+ }
117
+
118
+ .notification[data-tone='info'] {
119
+ --notification-accent: #2563eb;
120
+ }
121
+
122
+ .notification[data-tone='success'] {
123
+ --notification-accent: #16a34a;
124
+ }
125
+
126
+ .notification[data-tone='warning'] {
127
+ --notification-accent: #d97706;
128
+ }
129
+
130
+ .notification[data-tone='danger'] {
131
+ --notification-accent: #dc2626;
132
+ }
133
+
134
+ .notification-title,
135
+ .notification-message {
136
+ margin: 0;
137
+ }
138
+
139
+ .notification-title {
140
+ font-weight: 600;
141
+ line-height: 1.35;
142
+ }
143
+
144
+ .notification-message {
145
+ margin-top: 0.125rem;
146
+ color: var(--sveltely-text-secondary-color);
147
+ font-size: 0.875em;
148
+ line-height: 1.45;
149
+ }
150
+
151
+ .notifications-empty {
152
+ color: var(--sveltely-text-secondary-color);
153
+ font-size: 0.875em;
154
+ }
155
+ </style>
@@ -0,0 +1,35 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ import { type LayoutProps } from '../../../style/layout';
4
+ import { type StyleProps } from '../../../style/surface';
5
+ import type { NotificationItem } from './types';
6
+ declare function $$render<TItem extends NotificationItem = NotificationItem>(): {
7
+ props: {
8
+ items?: TItem[];
9
+ children?: Snippet<[TItem, number]>;
10
+ empty?: Snippet;
11
+ key?: (item: TItem, index: number) => string | number;
12
+ animationDuration?: number;
13
+ } & LayoutProps & StyleProps & Omit<HTMLAttributes<HTMLDivElement>, "style" | "class" | "children">;
14
+ exports: {};
15
+ bindings: "items";
16
+ slots: {};
17
+ events: {};
18
+ };
19
+ declare class __sveltets_Render<TItem extends NotificationItem = NotificationItem> {
20
+ props(): ReturnType<typeof $$render<TItem>>['props'];
21
+ events(): ReturnType<typeof $$render<TItem>>['events'];
22
+ slots(): ReturnType<typeof $$render<TItem>>['slots'];
23
+ bindings(): "items";
24
+ exports(): {};
25
+ }
26
+ interface $$IsomorphicComponent {
27
+ new <TItem extends NotificationItem = NotificationItem>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<TItem>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<TItem>['props']>, ReturnType<__sveltets_Render<TItem>['events']>, ReturnType<__sveltets_Render<TItem>['slots']>> & {
28
+ $$bindings?: ReturnType<__sveltets_Render<TItem>['bindings']>;
29
+ } & ReturnType<__sveltets_Render<TItem>['exports']>;
30
+ <TItem extends NotificationItem = NotificationItem>(internal: unknown, props: ReturnType<__sveltets_Render<TItem>['props']> & {}): ReturnType<__sveltets_Render<TItem>['exports']>;
31
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
32
+ }
33
+ declare const Notifications: $$IsomorphicComponent;
34
+ type Notifications<TItem extends NotificationItem = NotificationItem> = InstanceType<typeof Notifications<TItem>>;
35
+ export default Notifications;
@@ -0,0 +1,2 @@
1
+ export { default } from './Notifications.svelte';
2
+ export type { NotificationItem, NotificationTone } from './types';
@@ -0,0 +1 @@
1
+ export { default } from './Notifications.svelte';
@@ -0,0 +1,8 @@
1
+ export type NotificationTone = 'default' | 'info' | 'success' | 'warning' | 'danger';
2
+ export type NotificationItem = {
3
+ id?: string | number;
4
+ title?: string;
5
+ message?: string;
6
+ description?: string;
7
+ tone?: NotificationTone;
8
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -120,25 +120,27 @@
120
120
  data-disabled={disabled ? 'true' : 'false'}
121
121
  data-error={error ? 'true' : 'false'}
122
122
  >
123
- <input
124
- value={draftValue}
125
- type="number"
126
- {disabled}
127
- {name}
128
- {autocomplete}
129
- {required}
130
- {readonly}
131
- {inputmode}
132
- {placeholder}
133
- {min}
134
- {max}
135
- {step}
136
- aria-invalid={error ? 'true' : undefined}
137
- aria-describedby={describedBy}
138
- class="text-field-input"
139
- oninput={handleInput}
140
- onblur={handleBlur}
141
- />
123
+ <div class="text-field-control">
124
+ <input
125
+ value={draftValue}
126
+ type="number"
127
+ {disabled}
128
+ {name}
129
+ {autocomplete}
130
+ {required}
131
+ {readonly}
132
+ {inputmode}
133
+ {placeholder}
134
+ {min}
135
+ {max}
136
+ {step}
137
+ aria-invalid={error ? 'true' : undefined}
138
+ aria-describedby={describedBy}
139
+ class="text-field-input"
140
+ oninput={handleInput}
141
+ onblur={handleBlur}
142
+ />
143
+ </div>
142
144
 
143
145
  {#if help}
144
146
  <span id="text-field-message" class="text-field-message">{help}</span>
@@ -156,6 +158,10 @@
156
158
  font-size: var(--text-field-font-size, var(--sveltely-font-size));
157
159
  }
158
160
 
161
+ .text-field-control {
162
+ display: contents;
163
+ }
164
+
159
165
  .text-field-input {
160
166
  width: 100%;
161
167
  min-width: 0;
@@ -7,6 +7,7 @@
7
7
  </script>
8
8
 
9
9
  <script lang="ts">
10
+ import HStack from '../HStack';
10
11
  import Pagination from './Pagination.svelte';
11
12
 
12
13
  let data = $state({
@@ -21,6 +22,6 @@
21
22
  };
22
23
  </script>
23
24
 
24
- <div class="w-fit">
25
+ <HStack>
25
26
  <Pagination {data} showFirstLast={true} onPageChange={(page) => onPageChange(page)} />
26
- </div>
27
+ </HStack>
@@ -157,9 +157,8 @@
157
157
  .pagination {
158
158
  --pagination-font-size: calc(var(--sveltely-font-size) * 0.875);
159
159
  --pagination-scale: calc(var(--pagination-font-size) / 0.875rem);
160
- --pagination-icon-size: calc(var(--pagination-font-size) * 1.143);
160
+ --pagination-icon-size: calc(var(--pagination-font-size) * 0.95);
161
161
  --pagination-gap: calc(var(--sveltely-gap) * 2);
162
- --pagination-icon-shift: calc(var(--sveltely-gap) / 2);
163
162
  display: inline-flex;
164
163
  width: fit-content;
165
164
  gap: var(--pagination-gap);
@@ -173,18 +172,12 @@
173
172
  }
174
173
 
175
174
  .pagination-icon {
175
+ display: block;
176
+ flex: 0 0 auto;
176
177
  width: var(--pagination-icon-size);
177
178
  height: var(--pagination-icon-size);
178
179
  }
179
180
 
180
- .pagination-icon-backward {
181
- transform: translateX(calc(var(--pagination-icon-shift) * -1));
182
- }
183
-
184
- .pagination-icon-forward {
185
- transform: translateX(var(--pagination-icon-shift));
186
- }
187
-
188
181
  .pagination-button {
189
182
  border-radius: var(--sveltely-border-radius);
190
183
  background: var(--sveltely-control-inactive-color);
@@ -193,14 +186,9 @@
193
186
  }
194
187
 
195
188
  .pagination-button-icon {
196
- padding: calc(var(--sveltely-padding-y) * 0.67 * var(--pagination-scale))
197
- calc(var(--sveltely-padding-x) * 0.67 * var(--pagination-scale));
198
- min-width: calc(
199
- (var(--sveltely-padding-x) * 1.34 * var(--pagination-scale)) + var(--pagination-icon-size)
200
- );
201
- min-height: calc(
202
- (var(--sveltely-padding-y) * 1.34 * var(--pagination-scale)) + var(--pagination-icon-size)
203
- );
189
+ width: calc(var(--pagination-icon-size) + var(--sveltely-padding-x) * var(--pagination-scale));
190
+ height: calc(var(--pagination-icon-size) + var(--sveltely-padding-y) * var(--pagination-scale));
191
+ padding: 0;
204
192
  }
205
193
 
206
194
  .pagination-button:disabled {
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { portalContent } from '../../../actions/portal';
2
+ import { Content as PortalContent } from '../Portal';
3
3
  import { getOpenPopoverIds, getParentId } from './registry.svelte';
4
4
 
5
5
  type Point = { x: number; y: number };
@@ -96,7 +96,7 @@
96
96
  </script>
97
97
 
98
98
  {#if geometries.length > 0}
99
- <div use:portalContent class="pointer-events-none fixed inset-0 z-[9999]">
99
+ <PortalContent class="pointer-events-none fixed inset-0 z-[9999]">
100
100
  <svg width="100%" height="100%" aria-hidden="true">
101
101
  {#each geometries as geo (geo.id)}
102
102
  <!-- Safe polygon (rendered first so rects draw over it) -->
@@ -159,5 +159,5 @@
159
159
  </text>
160
160
  {/each}
161
161
  </svg>
162
- </div>
162
+ </PortalContent>
163
163
  {/if}
@@ -0,0 +1,20 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLAttributes } from 'svelte/elements';
4
+ import { portalContent } from '../../../actions/portal';
5
+
6
+ type Props = {
7
+ id?: string;
8
+ children?: Snippet;
9
+ element?: HTMLElement | null;
10
+ } & Omit<HTMLAttributes<HTMLDivElement>, 'children'>;
11
+
12
+ let { id = 'default', children, element = $bindable<HTMLElement | null>(null), ...props }: Props =
13
+ $props();
14
+ </script>
15
+
16
+ <div bind:this={element} use:portalContent={id} {...props}>
17
+ {#if children}
18
+ {@render children()}
19
+ {/if}
20
+ </div>
@@ -0,0 +1,10 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ type Props = {
4
+ id?: string;
5
+ children?: Snippet;
6
+ element?: HTMLElement | null;
7
+ } & Omit<HTMLAttributes<HTMLDivElement>, 'children'>;
8
+ declare const Content: import("svelte").Component<Props, {}, "element">;
9
+ type Content = ReturnType<typeof Content>;
10
+ export default Content;
@@ -1,3 +1,7 @@
1
+ <script module lang="ts">
2
+ export { default as Content } from './Content.svelte';
3
+ </script>
4
+
1
5
  <script lang="ts">
2
6
  import { portalHost } from '../../../actions/portal';
3
7
 
@@ -1,3 +1,4 @@
1
+ export { default as Content } from './Content.svelte';
1
2
  type Props = {
2
3
  id?: string;
3
4
  };
@@ -1 +1,2 @@
1
1
  export { default } from './Portal.svelte';
2
+ export { default as Content } from './Content.svelte';
@@ -1 +1,2 @@
1
1
  export { default } from './Portal.svelte';
2
+ export { default as Content } from './Content.svelte';
@@ -48,12 +48,18 @@
48
48
  };
49
49
 
50
50
  export type ScrollViewOverscrollBehavior = 'auto' | 'contain' | 'none';
51
+ export type ScrollContentSize = {
52
+ width: number;
53
+ height: number;
54
+ };
51
55
 
52
56
  type Props = {
53
- children: Snippet;
57
+ children?: Snippet;
58
+ layout?: Snippet<[geometry: ScrollGeometry]>;
54
59
  viewport?: ScrollViewHandle | null;
55
60
  axis?: ScrollAxis;
56
61
  contentStyles?: StyleProps;
62
+ contentSize?: ScrollContentSize | null;
57
63
  onScroll?: (geometry: ScrollGeometry) => void;
58
64
  scrollGradient?: boolean;
59
65
  scrollGradientSize?: number | string;
@@ -65,16 +71,18 @@
65
71
 
66
72
  let {
67
73
  children,
74
+ layout,
68
75
  viewport = $bindable<ScrollViewHandle | null>(null),
69
76
  axis = 'vertical',
70
77
  contentStyles = {},
78
+ contentSize = null,
71
79
  onScroll,
72
80
  scrollGradient = true,
73
81
  scrollGradientSize = '2.5rem',
74
82
  showIndicators = true,
75
83
  overscrollBehavior = 'contain',
76
84
  ...restProps
77
- }: Props = $props();
85
+ }: Props & Record<string, unknown> = $props();
78
86
 
79
87
  const extractedLoaderProps = $derived.by(() => extractLoaderProps(restProps));
80
88
  const loaderProps = $derived(extractedLoaderProps.loaderProps);
@@ -108,16 +116,26 @@
108
116
  let thumbYOffset = $state(0);
109
117
  let resizeObserver: ResizeObserver | null = null;
110
118
  let mutationObserver: MutationObserver | null = null;
119
+ let scrollAnimationFrame: number | null = null;
111
120
  const scrollGradientEnabled = $derived(scrollGradient && axis !== 'horizontal');
112
121
  const verticalScrollbarEnabled = $derived(showIndicators && axis !== 'horizontal' && canScrollY);
113
122
  const horizontalScrollbarEnabled = $derived(showIndicators && axis !== 'vertical' && canScrollX);
123
+ const syntheticContentStyle = $derived.by(() => {
124
+ if (!contentSize) return '';
125
+ return `width: ${contentSize.width}px; height: ${contentSize.height}px; min-width: ${contentSize.width}px; min-height: ${contentSize.height}px;`;
126
+ });
114
127
 
115
128
  const measuredContentSize = () => {
129
+ if (contentSize) return contentSize;
116
130
  if (!contentElement) return { width: 0, height: 0 };
117
131
 
118
132
  const contentRect = contentElement.getBoundingClientRect();
119
133
  let width = Math.max(contentElement.scrollWidth, contentElement.offsetWidth, contentRect.width);
120
- let height = Math.max(contentElement.scrollHeight, contentElement.offsetHeight, contentRect.height);
134
+ let height = Math.max(
135
+ contentElement.scrollHeight,
136
+ contentElement.offsetHeight,
137
+ contentRect.height
138
+ );
121
139
 
122
140
  for (const child of Array.from(contentElement.children)) {
123
141
  const childElement = child as HTMLElement;
@@ -165,6 +183,7 @@
165
183
  }
166
184
  };
167
185
  }
186
+ const scrollGeometry = $derived.by(() => getScrollGeometry());
168
187
 
169
188
  function syncScrollbarGeometry() {
170
189
  if (!viewportElement || !contentElement) {
@@ -220,7 +239,14 @@
220
239
  onScroll?.(getScrollGeometry());
221
240
  }
222
241
 
242
+ function stopScrollAnimation() {
243
+ if (scrollAnimationFrame === null || typeof cancelAnimationFrame === 'undefined') return;
244
+ cancelAnimationFrame(scrollAnimationFrame);
245
+ scrollAnimationFrame = null;
246
+ }
247
+
223
248
  function scrollTo(nextX: number, nextY: number) {
249
+ stopScrollAnimation();
224
250
  const previousX = offsetX;
225
251
  const previousY = offsetY;
226
252
  offsetX = clamp(axis === 'vertical' ? 0 : nextX, 0, maxOffsetX());
@@ -229,6 +255,41 @@
229
255
  return offsetX !== previousX || offsetY !== previousY;
230
256
  }
231
257
 
258
+ function animateScrollTo(nextX: number, nextY: number) {
259
+ if (typeof requestAnimationFrame === 'undefined') {
260
+ scrollTo(nextX, nextY);
261
+ return;
262
+ }
263
+
264
+ stopScrollAnimation();
265
+ const fromX = offsetX;
266
+ const fromY = offsetY;
267
+ const toX = clamp(axis === 'vertical' ? 0 : nextX, 0, maxOffsetX());
268
+ const toY = clamp(axis === 'horizontal' ? 0 : nextY, 0, maxOffsetY());
269
+ const duration = 260;
270
+ const startedAt = performance.now();
271
+ const easeOutCubic = (value: number) => 1 - Math.pow(1 - value, 3);
272
+
273
+ const step = (now: number) => {
274
+ const progress = Math.min(1, (now - startedAt) / duration);
275
+ const eased = easeOutCubic(progress);
276
+ offsetX = fromX + (toX - fromX) * eased;
277
+ offsetY = fromY + (toY - fromY) * eased;
278
+ publishScroll();
279
+
280
+ if (progress < 1) {
281
+ scrollAnimationFrame = requestAnimationFrame(step);
282
+ } else {
283
+ scrollAnimationFrame = null;
284
+ offsetX = toX;
285
+ offsetY = toY;
286
+ publishScroll();
287
+ }
288
+ };
289
+
290
+ scrollAnimationFrame = requestAnimationFrame(step);
291
+ }
292
+
232
293
  function shouldContainOverscroll(consumed: boolean) {
233
294
  if (overscrollBehavior === 'none') return true;
234
295
  return overscrollBehavior === 'contain' && consumed;
@@ -270,13 +331,20 @@
270
331
  scrollTo(options, y ?? offsetY);
271
332
  return;
272
333
  }
334
+ if (options.behavior === 'smooth') {
335
+ animateScrollTo(options.left ?? offsetX, options.top ?? offsetY);
336
+ return;
337
+ }
273
338
  scrollTo(options.left ?? offsetX, options.top ?? offsetY);
274
339
  }
275
340
  };
276
341
 
277
342
  function handleWheel(event: WheelEvent) {
278
- const wheelX = axis === 'horizontal' && event.deltaX === 0 ? event.deltaY : event.deltaX;
279
- const consumed = scrollTo(offsetX + wheelX, offsetY + event.deltaY);
343
+ const shouldMapWheelYToX =
344
+ event.deltaX === 0 && (axis === 'horizontal' || (axis === 'both' && event.shiftKey));
345
+ const wheelX = shouldMapWheelYToX ? event.deltaY : event.deltaX;
346
+ const wheelY = axis === 'both' && event.shiftKey ? 0 : event.deltaY;
347
+ const consumed = scrollTo(offsetX + wheelX, offsetY + wheelY);
280
348
 
281
349
  if (consumed || shouldContainOverscroll(consumed)) {
282
350
  event.preventDefault();
@@ -333,6 +401,7 @@
333
401
  });
334
402
 
335
403
  return () => {
404
+ stopScrollAnimation();
336
405
  resizeObserver?.disconnect();
337
406
  resizeObserver = null;
338
407
  mutationObserver?.disconnect();
@@ -363,7 +432,8 @@
363
432
  const thumbSize = orientation === 'horizontal' ? thumbXSize : thumbYSize;
364
433
  const maxScroll = Math.max(0, contentSize - viewportSize);
365
434
  const maxThumbOffset = Math.max(1, viewportSize - thumbSize);
366
- const delta = (orientation === 'horizontal' ? moveEvent.clientX : moveEvent.clientY) - startPointer;
435
+ const delta =
436
+ (orientation === 'horizontal' ? moveEvent.clientX : moveEvent.clientY) - startPointer;
367
437
  const nextScroll = startScroll + (delta / maxThumbOffset) * maxScroll;
368
438
 
369
439
  if (orientation === 'horizontal') {
@@ -383,7 +453,6 @@
383
453
  window.addEventListener('pointerup', up);
384
454
  window.addEventListener('pointercancel', up);
385
455
  }
386
-
387
456
  </script>
388
457
 
389
458
  <div
@@ -414,9 +483,14 @@
414
483
  <div
415
484
  bind:this={contentElement}
416
485
  class="scroll-view-content"
417
- style={`${contentStyle} transform: translate3d(${-offsetX}px, ${-offsetY}px, 0);`}
486
+ class:scroll-view-content-engine={Boolean(layout)}
487
+ style={`${contentStyle} ${syntheticContentStyle} transform: translate3d(${-offsetX}px, ${-offsetY}px, 0);`}
418
488
  >
419
- {@render children()}
489
+ {#if layout}
490
+ {@render layout(scrollGeometry)}
491
+ {:else if children}
492
+ {@render children()}
493
+ {/if}
420
494
  </div>
421
495
  </div>
422
496
  {#if scrollGradientEnabled}
@@ -481,6 +555,11 @@
481
555
  will-change: transform;
482
556
  }
483
557
 
558
+ .scroll-view-content-engine {
559
+ position: relative;
560
+ padding: 0;
561
+ }
562
+
484
563
  .scroll-view-horizontal .scroll-view-content {
485
564
  min-height: 100%;
486
565
  width: max-content;
@@ -42,17 +42,24 @@ export type ScrollViewHandle = {
42
42
  scrollTo: (options: ScrollToOptions | number, y?: number) => void;
43
43
  };
44
44
  export type ScrollViewOverscrollBehavior = 'auto' | 'contain' | 'none';
45
+ export type ScrollContentSize = {
46
+ width: number;
47
+ height: number;
48
+ };
45
49
  type Props = {
46
- children: Snippet;
50
+ children?: Snippet;
51
+ layout?: Snippet<[geometry: ScrollGeometry]>;
47
52
  viewport?: ScrollViewHandle | null;
48
53
  axis?: ScrollAxis;
49
54
  contentStyles?: StyleProps;
55
+ contentSize?: ScrollContentSize | null;
50
56
  onScroll?: (geometry: ScrollGeometry) => void;
51
57
  scrollGradient?: boolean;
52
58
  scrollGradientSize?: number | string;
53
59
  showIndicators?: boolean;
54
60
  overscrollBehavior?: ScrollViewOverscrollBehavior;
55
61
  } & LayoutProps & StyleProps & LoaderProps;
56
- declare const ScrollView: import("svelte").Component<Props, {}, "viewport">;
62
+ type $$ComponentProps = Props & Record<string, unknown>;
63
+ declare const ScrollView: import("svelte").Component<$$ComponentProps, {}, "viewport">;
57
64
  type ScrollView = ReturnType<typeof ScrollView>;
58
65
  export default ScrollView;
@@ -1,2 +1,2 @@
1
1
  export { default } from './ScrollView.svelte';
2
- export type { ScrollGeometry, ScrollViewHandle, ScrollViewOverscrollBehavior } from './ScrollView.svelte';
2
+ export type { ScrollGeometry, ScrollContentSize, ScrollViewHandle, ScrollViewOverscrollBehavior } from './ScrollView.svelte';
@@ -6,12 +6,13 @@
6
6
  </script>
7
7
 
8
8
  <script lang="ts">
9
+ import VStack from '../VStack';
9
10
  import SearchField from './SearchField.svelte';
10
11
 
11
12
  let value = $state('');
12
13
  </script>
13
14
 
14
- <div class="vstack w-full max-w-sm gap-2">
15
+ <VStack width="24rem" gap={5}>
15
16
  <SearchField bind:value placeholder="Search components..." />
16
17
  <p class="text-xs text-[var(--sveltely-text-secondary-color)]">Value: {value || 'empty'}</p>
17
- </div>
18
+ </VStack>