@delightstack/components 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +136 -0
  3. package/SKILL.md +149 -0
  4. package/bin/agents.js +63 -0
  5. package/dist/actions/Alert.svelte +202 -0
  6. package/dist/actions/Alert.svelte.d.ts +36 -0
  7. package/dist/actions/Alert.svelte.d.ts.map +1 -0
  8. package/dist/actions/Button.svelte +1450 -0
  9. package/dist/actions/Button.svelte.d.ts +56 -0
  10. package/dist/actions/Button.svelte.d.ts.map +1 -0
  11. package/dist/actions/ButtonGroup.svelte +111 -0
  12. package/dist/actions/ButtonGroup.svelte.d.ts +41 -0
  13. package/dist/actions/ButtonGroup.svelte.d.ts.map +1 -0
  14. package/dist/actions/CommandPalette.svelte +939 -0
  15. package/dist/actions/CommandPalette.svelte.d.ts +37 -0
  16. package/dist/actions/CommandPalette.svelte.d.ts.map +1 -0
  17. package/dist/actions/ContextMenu.svelte +138 -0
  18. package/dist/actions/ContextMenu.svelte.d.ts +54 -0
  19. package/dist/actions/ContextMenu.svelte.d.ts.map +1 -0
  20. package/dist/actions/Modal.svelte +474 -0
  21. package/dist/actions/Modal.svelte.d.ts +28 -0
  22. package/dist/actions/Modal.svelte.d.ts.map +1 -0
  23. package/dist/actions/Popover.svelte +1214 -0
  24. package/dist/actions/Popover.svelte.d.ts +31 -0
  25. package/dist/actions/Popover.svelte.d.ts.map +1 -0
  26. package/dist/actions/Portal.svelte +80 -0
  27. package/dist/actions/Portal.svelte.d.ts +17 -0
  28. package/dist/actions/Portal.svelte.d.ts.map +1 -0
  29. package/dist/actions/ThemeToggle.svelte +345 -0
  30. package/dist/actions/ThemeToggle.svelte.d.ts +15 -0
  31. package/dist/actions/ThemeToggle.svelte.d.ts.map +1 -0
  32. package/dist/actions/index.d.ts +13 -0
  33. package/dist/actions/index.d.ts.map +1 -0
  34. package/dist/actions/index.js +10 -0
  35. package/dist/actions/scrollbar.d.ts +48 -0
  36. package/dist/actions/scrollbar.d.ts.map +1 -0
  37. package/dist/actions/scrollbar.js +404 -0
  38. package/dist/display/Accordion.svelte +586 -0
  39. package/dist/display/Accordion.svelte.d.ts +41 -0
  40. package/dist/display/Accordion.svelte.d.ts.map +1 -0
  41. package/dist/display/Avatar.svelte +527 -0
  42. package/dist/display/Avatar.svelte.d.ts +22 -0
  43. package/dist/display/Avatar.svelte.d.ts.map +1 -0
  44. package/dist/display/AvatarGroup.svelte +298 -0
  45. package/dist/display/AvatarGroup.svelte.d.ts +31 -0
  46. package/dist/display/AvatarGroup.svelte.d.ts.map +1 -0
  47. package/dist/display/Calendar.svelte +1366 -0
  48. package/dist/display/Calendar.svelte.d.ts +58 -0
  49. package/dist/display/Calendar.svelte.d.ts.map +1 -0
  50. package/dist/display/Chart.svelte +1426 -0
  51. package/dist/display/Chart.svelte.d.ts +35 -0
  52. package/dist/display/Chart.svelte.d.ts.map +1 -0
  53. package/dist/display/Code.svelte +780 -0
  54. package/dist/display/Code.svelte.d.ts +19 -0
  55. package/dist/display/Code.svelte.d.ts.map +1 -0
  56. package/dist/display/Comparison.svelte +686 -0
  57. package/dist/display/Comparison.svelte.d.ts +22 -0
  58. package/dist/display/Comparison.svelte.d.ts.map +1 -0
  59. package/dist/display/Counter.svelte +285 -0
  60. package/dist/display/Counter.svelte.d.ts +21 -0
  61. package/dist/display/Counter.svelte.d.ts.map +1 -0
  62. package/dist/display/Expand.svelte +48 -0
  63. package/dist/display/Expand.svelte.d.ts +9 -0
  64. package/dist/display/Expand.svelte.d.ts.map +1 -0
  65. package/dist/display/List.svelte +294 -0
  66. package/dist/display/List.svelte.d.ts +40 -0
  67. package/dist/display/List.svelte.d.ts.map +1 -0
  68. package/dist/display/ListContextReset.svelte +19 -0
  69. package/dist/display/ListContextReset.svelte.d.ts +7 -0
  70. package/dist/display/ListContextReset.svelte.d.ts.map +1 -0
  71. package/dist/display/ListItem.svelte +834 -0
  72. package/dist/display/ListItem.svelte.d.ts +22 -0
  73. package/dist/display/ListItem.svelte.d.ts.map +1 -0
  74. package/dist/display/QR.svelte +1193 -0
  75. package/dist/display/QR.svelte.d.ts +23 -0
  76. package/dist/display/QR.svelte.d.ts.map +1 -0
  77. package/dist/display/SplitPane.svelte +744 -0
  78. package/dist/display/SplitPane.svelte.d.ts +25 -0
  79. package/dist/display/SplitPane.svelte.d.ts.map +1 -0
  80. package/dist/display/Stat.svelte +439 -0
  81. package/dist/display/Stat.svelte.d.ts +24 -0
  82. package/dist/display/Stat.svelte.d.ts.map +1 -0
  83. package/dist/display/Table.svelte +4654 -0
  84. package/dist/display/Table.svelte.d.ts +249 -0
  85. package/dist/display/Table.svelte.d.ts.map +1 -0
  86. package/dist/display/TableCellEditor.svelte +935 -0
  87. package/dist/display/TableCellEditor.svelte.d.ts +58 -0
  88. package/dist/display/TableCellEditor.svelte.d.ts.map +1 -0
  89. package/dist/display/Timeline.svelte +1258 -0
  90. package/dist/display/Timeline.svelte.d.ts +43 -0
  91. package/dist/display/Timeline.svelte.d.ts.map +1 -0
  92. package/dist/display/Tree.svelte +1740 -0
  93. package/dist/display/Tree.svelte.d.ts +74 -0
  94. package/dist/display/Tree.svelte.d.ts.map +1 -0
  95. package/dist/display/Typewriter.svelte +338 -0
  96. package/dist/display/Typewriter.svelte.d.ts +22 -0
  97. package/dist/display/Typewriter.svelte.d.ts.map +1 -0
  98. package/dist/display/index.d.ts +24 -0
  99. package/dist/display/index.d.ts.map +1 -0
  100. package/dist/display/index.js +18 -0
  101. package/dist/feedback/Callout.svelte +529 -0
  102. package/dist/feedback/Callout.svelte.d.ts +24 -0
  103. package/dist/feedback/Callout.svelte.d.ts.map +1 -0
  104. package/dist/feedback/Confetti.svelte +631 -0
  105. package/dist/feedback/Confetti.svelte.d.ts +90 -0
  106. package/dist/feedback/Confetti.svelte.d.ts.map +1 -0
  107. package/dist/feedback/Progress.svelte +382 -0
  108. package/dist/feedback/Progress.svelte.d.ts +25 -0
  109. package/dist/feedback/Progress.svelte.d.ts.map +1 -0
  110. package/dist/feedback/Toast.svelte +967 -0
  111. package/dist/feedback/Toast.svelte.d.ts +54 -0
  112. package/dist/feedback/Toast.svelte.d.ts.map +1 -0
  113. package/dist/feedback/index.d.ts +7 -0
  114. package/dist/feedback/index.d.ts.map +1 -0
  115. package/dist/feedback/index.js +4 -0
  116. package/dist/form/Checkbox.svelte +449 -0
  117. package/dist/form/Checkbox.svelte.d.ts +27 -0
  118. package/dist/form/Checkbox.svelte.d.ts.map +1 -0
  119. package/dist/form/Fieldset.svelte +410 -0
  120. package/dist/form/Fieldset.svelte.d.ts +22 -0
  121. package/dist/form/Fieldset.svelte.d.ts.map +1 -0
  122. package/dist/form/FileUpload.svelte +934 -0
  123. package/dist/form/FileUpload.svelte.d.ts +41 -0
  124. package/dist/form/FileUpload.svelte.d.ts.map +1 -0
  125. package/dist/form/Form.svelte +530 -0
  126. package/dist/form/Form.svelte.d.ts +120 -0
  127. package/dist/form/Form.svelte.d.ts.map +1 -0
  128. package/dist/form/Input.svelte +2858 -0
  129. package/dist/form/Input.svelte.d.ts +66 -0
  130. package/dist/form/Input.svelte.d.ts.map +1 -0
  131. package/dist/form/Radio.svelte +507 -0
  132. package/dist/form/Radio.svelte.d.ts +39 -0
  133. package/dist/form/Radio.svelte.d.ts.map +1 -0
  134. package/dist/form/Range.svelte +912 -0
  135. package/dist/form/Range.svelte.d.ts +33 -0
  136. package/dist/form/Range.svelte.d.ts.map +1 -0
  137. package/dist/form/Rating.svelte +429 -0
  138. package/dist/form/Rating.svelte.d.ts +28 -0
  139. package/dist/form/Rating.svelte.d.ts.map +1 -0
  140. package/dist/form/Select.svelte +1933 -0
  141. package/dist/form/Select.svelte.d.ts +54 -0
  142. package/dist/form/Select.svelte.d.ts.map +1 -0
  143. package/dist/form/Toggle.svelte +645 -0
  144. package/dist/form/Toggle.svelte.d.ts +50 -0
  145. package/dist/form/Toggle.svelte.d.ts.map +1 -0
  146. package/dist/form/index.d.ts +15 -0
  147. package/dist/form/index.d.ts.map +1 -0
  148. package/dist/form/index.js +10 -0
  149. package/dist/index.d.ts +7 -0
  150. package/dist/index.d.ts.map +1 -0
  151. package/dist/index.js +6 -0
  152. package/dist/layout/README.md +172 -0
  153. package/dist/media/Carousel.svelte +2424 -0
  154. package/dist/media/Carousel.svelte.d.ts +47 -0
  155. package/dist/media/Carousel.svelte.d.ts.map +1 -0
  156. package/dist/media/Gallery.svelte +2881 -0
  157. package/dist/media/Gallery.svelte.d.ts +82 -0
  158. package/dist/media/Gallery.svelte.d.ts.map +1 -0
  159. package/dist/media/Image.svelte +389 -0
  160. package/dist/media/Image.svelte.d.ts +33 -0
  161. package/dist/media/Image.svelte.d.ts.map +1 -0
  162. package/dist/media/PDF.svelte +1793 -0
  163. package/dist/media/PDF.svelte.d.ts +44 -0
  164. package/dist/media/PDF.svelte.d.ts.map +1 -0
  165. package/dist/media/Panorama.svelte +1391 -0
  166. package/dist/media/Panorama.svelte.d.ts +47 -0
  167. package/dist/media/Panorama.svelte.d.ts.map +1 -0
  168. package/dist/media/Video.svelte +2501 -0
  169. package/dist/media/Video.svelte.d.ts +58 -0
  170. package/dist/media/Video.svelte.d.ts.map +1 -0
  171. package/dist/media/carousel.d.ts +211 -0
  172. package/dist/media/carousel.d.ts.map +1 -0
  173. package/dist/media/carousel.js +408 -0
  174. package/dist/media/index.d.ts +11 -0
  175. package/dist/media/index.d.ts.map +1 -0
  176. package/dist/media/index.js +5 -0
  177. package/dist/navigation/BottomSheet.svelte +636 -0
  178. package/dist/navigation/BottomSheet.svelte.d.ts +27 -0
  179. package/dist/navigation/BottomSheet.svelte.d.ts.map +1 -0
  180. package/dist/navigation/Breadcrumbs.svelte +611 -0
  181. package/dist/navigation/Breadcrumbs.svelte.d.ts +28 -0
  182. package/dist/navigation/Breadcrumbs.svelte.d.ts.map +1 -0
  183. package/dist/navigation/Pagination.svelte +641 -0
  184. package/dist/navigation/Pagination.svelte.d.ts +27 -0
  185. package/dist/navigation/Pagination.svelte.d.ts.map +1 -0
  186. package/dist/navigation/Steps.svelte +965 -0
  187. package/dist/navigation/Steps.svelte.d.ts +43 -0
  188. package/dist/navigation/Steps.svelte.d.ts.map +1 -0
  189. package/dist/navigation/Tabs.svelte +698 -0
  190. package/dist/navigation/Tabs.svelte.d.ts +41 -0
  191. package/dist/navigation/Tabs.svelte.d.ts.map +1 -0
  192. package/dist/navigation/index.d.ts +8 -0
  193. package/dist/navigation/index.d.ts.map +1 -0
  194. package/dist/navigation/index.js +5 -0
  195. package/package.json +139 -0
@@ -0,0 +1,404 @@
1
+ /** Overflow values that make an element scrollable */
2
+ const SCROLLABLE = /auto|scroll|overlay/;
3
+ /** Minimum thumb length (px), so the thumb stays grabbable in huge documents */
4
+ const MIN_THUMB = 32;
5
+ let styles_injected = false;
6
+ /**
7
+ * The shared stylesheet for every scrollbar attachment. Visual styling lives
8
+ * here (driven by the --scrollbar-* tokens); geometry is set inline by the
9
+ * attachment. The show transition follows the design system's hover rule:
10
+ * snap in, ease out.
11
+ */
12
+ function injectStyles() {
13
+ if (styles_injected || typeof document === 'undefined')
14
+ return;
15
+ styles_injected = true;
16
+ const style = document.createElement('style');
17
+ style.setAttribute('data-delight-scrollbar', '');
18
+ style.textContent = `
19
+ [data-scrollbar] { scrollbar-width: none !important; }
20
+ [data-scrollbar]::-webkit-scrollbar { display: none !important; width: 0 !important; height: 0 !important; }
21
+ .delight-scrollbar {
22
+ position: absolute;
23
+ z-index: 10;
24
+ opacity: 0;
25
+ pointer-events: none;
26
+ transition: opacity 250ms var(--ease-out, ease);
27
+ }
28
+ .delight-scrollbar[data-show] {
29
+ opacity: 1;
30
+ pointer-events: auto;
31
+ transition: none;
32
+ }
33
+ .delight-scrollbar-thumb {
34
+ position: absolute;
35
+ border-radius: var(--radius-full, 1e5px);
36
+ background-color: var(--scrollbar-thumb-color, rgb(128 128 128 / 0.5));
37
+ transition:
38
+ background-color 250ms var(--ease-out, ease),
39
+ width 150ms var(--ease-out, ease),
40
+ height 150ms var(--ease-out, ease);
41
+ }
42
+ .delight-scrollbar[data-axis='y'] .delight-scrollbar-thumb {
43
+ right: var(--scrollbar-inset, 2px);
44
+ width: calc(var(--scrollbar-size, 10px) * 0.5);
45
+ }
46
+ .delight-scrollbar[data-axis='y'][data-rtl] .delight-scrollbar-thumb {
47
+ right: auto;
48
+ left: var(--scrollbar-inset, 2px);
49
+ }
50
+ .delight-scrollbar[data-axis='x'] .delight-scrollbar-thumb {
51
+ bottom: var(--scrollbar-inset, 2px);
52
+ height: calc(var(--scrollbar-size, 10px) * 0.5);
53
+ }
54
+ .delight-scrollbar:hover .delight-scrollbar-thumb,
55
+ .delight-scrollbar[data-dragging] .delight-scrollbar-thumb {
56
+ background-color: var(--scrollbar-thumb-color-active, rgb(128 128 128 / 0.8));
57
+ transition:
58
+ width 150ms var(--ease-out, ease),
59
+ height 150ms var(--ease-out, ease);
60
+ }
61
+ .delight-scrollbar[data-axis='y']:hover .delight-scrollbar-thumb,
62
+ .delight-scrollbar[data-axis='y'][data-dragging] .delight-scrollbar-thumb {
63
+ width: calc(var(--scrollbar-size, 10px) - var(--scrollbar-inset, 2px));
64
+ }
65
+ .delight-scrollbar[data-axis='x']:hover .delight-scrollbar-thumb,
66
+ .delight-scrollbar[data-axis='x'][data-dragging] .delight-scrollbar-thumb {
67
+ height: calc(var(--scrollbar-size, 10px) - var(--scrollbar-inset, 2px));
68
+ }
69
+ @media (prefers-reduced-motion: reduce) {
70
+ .delight-scrollbar,
71
+ .delight-scrollbar-thumb { transition: none !important; }
72
+ }
73
+ `;
74
+ document.head.appendChild(style);
75
+ }
76
+ function clamp(value, min, max) {
77
+ return Math.min(Math.max(value, min), max);
78
+ }
79
+ /**
80
+ * A svelte attachment that replaces an element's native scrollbars with the
81
+ * delightstack overlay scrollbar: a token-driven thumb that floats over the
82
+ * content (no layout gutter), fades in while scrolling/hovering and back out
83
+ * when idle, insets itself past the container's rounded corners, and supports
84
+ * dragging and click-to-jump like a native bar.
85
+ *
86
+ * Native scrolling (wheel, keyboard, touch, momentum) is untouched — the
87
+ * element keeps scrolling itself; only the visual scrollbar is replaced. On
88
+ * touch-only devices the attachment does nothing and the native auto-hiding
89
+ * indicators remain.
90
+ *
91
+ * The element's parent is used as the positioning context for the overlay
92
+ * (it is given `position: relative` if static), so the element should keep
93
+ * the same box as its parent or be positioned statically within it.
94
+ * @example
95
+ * ```svelte
96
+ * <div class="content" {@attach scrollbar()}>...</div>
97
+ * ```
98
+ */
99
+ export function scrollbar(options = {}) {
100
+ return (node) => {
101
+ if (typeof window === 'undefined')
102
+ return;
103
+ // Touch devices keep their native auto-hiding overlay indicators
104
+ if (!window.matchMedia('(hover: hover) and (pointer: fine)').matches)
105
+ return;
106
+ const parent_el = node.parentElement;
107
+ if (!parent_el)
108
+ return;
109
+ const parent = parent_el;
110
+ injectStyles();
111
+ const { autohide = true, autohide_delay = 1000, corner_inset, track_insets, } = options;
112
+ node.setAttribute('data-scrollbar', '');
113
+ // The tracks are positioned against the nearest containing block, which
114
+ // must be the parent for the offset math below to hold
115
+ let restore_position;
116
+ if (getComputedStyle(parent).position === 'static') {
117
+ restore_position = parent.style.position;
118
+ parent.style.position = 'relative';
119
+ }
120
+ function createBar(axis) {
121
+ const track = document.createElement('div');
122
+ track.className = 'delight-scrollbar';
123
+ track.setAttribute('data-axis', axis);
124
+ track.setAttribute('aria-hidden', 'true');
125
+ const thumb = document.createElement('div');
126
+ thumb.className = 'delight-scrollbar-thumb';
127
+ track.appendChild(thumb);
128
+ return {
129
+ axis,
130
+ track,
131
+ thumb,
132
+ enabled: false,
133
+ track_size: 0,
134
+ thumb_size: 0,
135
+ hovered: false,
136
+ dragging: false,
137
+ };
138
+ }
139
+ const bars = [createBar('y'), createBar('x')];
140
+ for (const bar of bars)
141
+ parent.insertBefore(bar.track, node.nextSibling);
142
+ let rtl = false;
143
+ let shown = false;
144
+ let hide_timer;
145
+ let frame;
146
+ function tokenPx(style, name, fallback) {
147
+ const value = parseFloat(style.getPropertyValue(name));
148
+ return Number.isFinite(value) ? value : fallback;
149
+ }
150
+ /** Repositions both tracks over the element's edges */
151
+ function layout() {
152
+ const style = getComputedStyle(node);
153
+ rtl = style.direction === 'rtl';
154
+ const size = tokenPx(style, '--scrollbar-size', 10);
155
+ const edge = tokenPx(style, '--scrollbar-inset', 2);
156
+ const track_width = size + edge;
157
+ const [y, x] = bars;
158
+ y.enabled =
159
+ SCROLLABLE.test(style.overflowY) && node.scrollHeight - node.clientHeight > 1;
160
+ x.enabled =
161
+ SCROLLABLE.test(style.overflowX) && node.scrollWidth - node.clientWidth > 1;
162
+ const top = node.offsetTop;
163
+ const left = node.offsetLeft;
164
+ const width = node.offsetWidth;
165
+ const height = node.offsetHeight;
166
+ // Rounded corners the thumb must stay clear of. The inset is HALF the
167
+ // computed radius — the thumb hugs the edge, so the curve has receded
168
+ // enough by then (and squircled corners double the computed radius).
169
+ // Scrollers often fill a rounded parent that carries the visual radius
170
+ // (modal/popover/card bodies), so when the element's own corner is
171
+ // square but it sits close to a rounded parent corner, inherit the
172
+ // parent's radius minus however far the element already sits from it.
173
+ const parent_style = getComputedStyle(parent);
174
+ const gap = {
175
+ top,
176
+ left,
177
+ right: parent.clientWidth - (left + width),
178
+ bottom: parent.clientHeight - (top + height),
179
+ };
180
+ function cornerRadius(corner) {
181
+ const own = parseFloat(style[`border${corner}Radius`]) || 0;
182
+ if (own > 0)
183
+ return own / 2;
184
+ const inherited = parseFloat(parent_style[`border${corner}Radius`]) || 0;
185
+ const [vertical_gap, horizontal_gap] = corner === 'TopLeft'
186
+ ? [gap.top, gap.left]
187
+ : corner === 'TopRight'
188
+ ? [gap.top, gap.right]
189
+ : corner === 'BottomLeft'
190
+ ? [gap.bottom, gap.left]
191
+ : [gap.bottom, gap.right];
192
+ return Math.max(0, inherited / 2 - Math.max(vertical_gap, horizontal_gap));
193
+ }
194
+ const radius = {
195
+ tl: cornerRadius('TopLeft'),
196
+ tr: cornerRadius('TopRight'),
197
+ bl: cornerRadius('BottomLeft'),
198
+ br: cornerRadius('BottomRight'),
199
+ };
200
+ const insets = typeof track_insets === 'function' ? track_insets(node) : (track_insets ?? {});
201
+ // Inset the track ends past the rounded corners (and past the other
202
+ // track when both axes are scrollable, so they never overlap)
203
+ const y_start = insets.top ?? corner_inset ?? (rtl ? radius.tl : radius.tr);
204
+ const y_end = (insets.bottom ?? corner_inset ?? (rtl ? radius.bl : radius.br)) +
205
+ (x.enabled ? track_width : 0);
206
+ if (rtl)
207
+ y.track.setAttribute('data-rtl', '');
208
+ else
209
+ y.track.removeAttribute('data-rtl');
210
+ y.track.style.top = `${top + y_start}px`;
211
+ y.track.style.height = `${Math.max(0, height - y_start - y_end)}px`;
212
+ y.track.style.width = `${track_width}px`;
213
+ y.track.style.left = rtl ? `${left}px` : `${left + width - track_width}px`;
214
+ y.track_size = Math.max(0, height - y_start - y_end);
215
+ const x_start = (insets.left ?? corner_inset ?? radius.bl) + (y.enabled && rtl ? track_width : 0);
216
+ const x_end = (insets.right ?? corner_inset ?? radius.br) +
217
+ (y.enabled && !rtl ? track_width : 0);
218
+ x.track.style.left = `${left + x_start}px`;
219
+ x.track.style.width = `${Math.max(0, width - x_start - x_end)}px`;
220
+ x.track.style.height = `${track_width}px`;
221
+ x.track.style.top = `${top + height - track_width}px`;
222
+ x.track_size = Math.max(0, width - x_start - x_end);
223
+ }
224
+ /** Syncs a thumb's size + position to the element's scroll state */
225
+ function updateThumb(bar) {
226
+ if (!bar.enabled)
227
+ return;
228
+ const vertical = bar.axis === 'y';
229
+ const scroll_size = vertical ? node.scrollHeight : node.scrollWidth;
230
+ const client_size = vertical ? node.clientHeight : node.clientWidth;
231
+ const max_scroll = scroll_size - client_size;
232
+ const min = Math.min(MIN_THUMB, bar.track_size / 2);
233
+ bar.thumb_size = clamp((client_size / scroll_size) * bar.track_size, min, bar.track_size);
234
+ const range = bar.track_size - bar.thumb_size;
235
+ const position = vertical ? node.scrollTop : node.scrollLeft;
236
+ let progress = max_scroll > 0 ? clamp(Math.abs(position) / max_scroll, 0, 1) : 0;
237
+ // In RTL, scrollLeft runs from 0 (content start, at the right) to
238
+ // -max_scroll, and the thumb travels right-to-left
239
+ if (!vertical && rtl)
240
+ progress = 1 - progress;
241
+ bar.thumb.style[vertical ? 'height' : 'width'] = `${bar.thumb_size}px`;
242
+ bar.thumb.style.transform = vertical
243
+ ? `translateY(${progress * range}px)`
244
+ : `translateX(${progress * range}px)`;
245
+ }
246
+ function syncVisibility() {
247
+ for (const bar of bars) {
248
+ const show = bar.enabled && (shown || !autohide);
249
+ if (show)
250
+ bar.track.setAttribute('data-show', '');
251
+ else
252
+ bar.track.removeAttribute('data-show');
253
+ }
254
+ }
255
+ function refresh() {
256
+ layout();
257
+ for (const bar of bars)
258
+ updateThumb(bar);
259
+ syncVisibility();
260
+ }
261
+ /** Batches refreshes from observers/scroll into one per frame */
262
+ function schedule() {
263
+ if (frame !== undefined)
264
+ return;
265
+ frame = requestAnimationFrame(() => {
266
+ frame = undefined;
267
+ refresh();
268
+ });
269
+ }
270
+ function scheduleHide() {
271
+ clearTimeout(hide_timer);
272
+ if (!autohide)
273
+ return;
274
+ hide_timer = setTimeout(() => {
275
+ if (bars.some((bar) => bar.hovered || bar.dragging))
276
+ return;
277
+ shown = false;
278
+ syncVisibility();
279
+ }, autohide_delay);
280
+ }
281
+ function show() {
282
+ if (!shown) {
283
+ shown = true;
284
+ syncVisibility();
285
+ }
286
+ scheduleHide();
287
+ }
288
+ function onScroll() {
289
+ for (const bar of bars)
290
+ updateThumb(bar);
291
+ show();
292
+ }
293
+ function setupBar(bar) {
294
+ const vertical = bar.axis === 'y';
295
+ bar.track.addEventListener('pointerenter', () => {
296
+ bar.hovered = true;
297
+ show();
298
+ });
299
+ bar.track.addEventListener('pointerleave', () => {
300
+ bar.hovered = false;
301
+ scheduleHide();
302
+ });
303
+ // The track sits over the content's edge, so forward wheel events the
304
+ // native scrollbar would have handled
305
+ bar.track.addEventListener('wheel', (event) => {
306
+ event.preventDefault();
307
+ node.scrollTop += event.deltaY;
308
+ node.scrollLeft += event.deltaX;
309
+ }, { passive: false });
310
+ // Click on the track (not the thumb) jumps to that position
311
+ bar.track.addEventListener('pointerdown', (event) => {
312
+ if (event.target !== bar.track || event.button !== 0)
313
+ return;
314
+ event.preventDefault();
315
+ const rect = bar.track.getBoundingClientRect();
316
+ const offset = vertical ? event.clientY - rect.top : event.clientX - rect.left;
317
+ const range = bar.track_size - bar.thumb_size;
318
+ let progress = range > 0 ? clamp((offset - bar.thumb_size / 2) / range, 0, 1) : 0;
319
+ if (!vertical && rtl)
320
+ progress = 1 - progress;
321
+ const scroll_size = vertical ? node.scrollHeight : node.scrollWidth;
322
+ const client_size = vertical ? node.clientHeight : node.clientWidth;
323
+ const target = progress * (scroll_size - client_size) * (!vertical && rtl ? -1 : 1);
324
+ node.scrollTo({ [vertical ? 'top' : 'left']: target, behavior: 'smooth' });
325
+ });
326
+ // Drag the thumb like a native scrollbar
327
+ bar.thumb.addEventListener('pointerdown', (event) => {
328
+ if (event.button !== 0)
329
+ return;
330
+ event.preventDefault();
331
+ event.stopPropagation();
332
+ bar.thumb.setPointerCapture(event.pointerId);
333
+ bar.dragging = true;
334
+ bar.track.setAttribute('data-dragging', '');
335
+ const start_pointer = vertical ? event.clientY : event.clientX;
336
+ const start_scroll = vertical ? node.scrollTop : node.scrollLeft;
337
+ function onMove(move) {
338
+ const delta = (vertical ? move.clientY : move.clientX) - start_pointer;
339
+ const range = bar.track_size - bar.thumb_size;
340
+ if (range <= 0)
341
+ return;
342
+ const scroll_size = vertical ? node.scrollHeight : node.scrollWidth;
343
+ const client_size = vertical ? node.clientHeight : node.clientWidth;
344
+ const ratio = (scroll_size - client_size) / range;
345
+ // The thumb→scroll mapping works out identically in RTL: both
346
+ // scrollLeft and the thumb's travel direction flip sign
347
+ const target = start_scroll + delta * ratio;
348
+ if (vertical)
349
+ node.scrollTop = target;
350
+ else
351
+ node.scrollLeft = target;
352
+ }
353
+ function onEnd(end) {
354
+ bar.thumb.releasePointerCapture(end.pointerId);
355
+ bar.thumb.removeEventListener('pointermove', onMove);
356
+ bar.thumb.removeEventListener('pointerup', onEnd);
357
+ bar.thumb.removeEventListener('pointercancel', onEnd);
358
+ bar.dragging = false;
359
+ bar.track.removeAttribute('data-dragging');
360
+ scheduleHide();
361
+ }
362
+ bar.thumb.addEventListener('pointermove', onMove);
363
+ bar.thumb.addEventListener('pointerup', onEnd);
364
+ bar.thumb.addEventListener('pointercancel', onEnd);
365
+ });
366
+ }
367
+ for (const bar of bars)
368
+ setupBar(bar);
369
+ node.addEventListener('scroll', onScroll, { passive: true });
370
+ node.addEventListener('pointerenter', show);
371
+ node.addEventListener('pointermove', show);
372
+ node.addEventListener('pointerleave', scheduleHide);
373
+ const resize_observer = new ResizeObserver(schedule);
374
+ resize_observer.observe(node);
375
+ // Content changes (rows added, text edited, …) change scrollHeight
376
+ // without resizing the container itself
377
+ const mutation_observer = new MutationObserver(schedule);
378
+ mutation_observer.observe(node, {
379
+ childList: true,
380
+ subtree: true,
381
+ characterData: true,
382
+ });
383
+ // Initial reveal so overflowing content advertises its scrollability
384
+ refresh();
385
+ if (bars.some((bar) => bar.enabled))
386
+ show();
387
+ return () => {
388
+ clearTimeout(hide_timer);
389
+ if (frame !== undefined)
390
+ cancelAnimationFrame(frame);
391
+ resize_observer.disconnect();
392
+ mutation_observer.disconnect();
393
+ node.removeEventListener('scroll', onScroll);
394
+ node.removeEventListener('pointerenter', show);
395
+ node.removeEventListener('pointermove', show);
396
+ node.removeEventListener('pointerleave', scheduleHide);
397
+ for (const bar of bars)
398
+ bar.track.remove();
399
+ node.removeAttribute('data-scrollbar');
400
+ if (restore_position !== undefined)
401
+ parent.style.position = restore_position;
402
+ };
403
+ };
404
+ }