@sticky-aside/core 0.0.0-stage → 1.0.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 maprox
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,16 @@
1
- # Temporary Holding Version
1
+ # @sticky-aside/core
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A short panel stays pinned to the top. A tall panel scrolls with the page, pins to the bottom while you scroll down, and pins to the top while you scroll up.
4
+
5
+ ```bash
6
+ npm install @sticky-aside/core
7
+ ```
8
+
9
+ ```ts
10
+ import { createStickyAside } from '@sticky-aside/core'
11
+ import '@sticky-aside/core/offsets.css'
12
+
13
+ const aside = createStickyAside({ wrapper, content })
14
+ ```
15
+
16
+ Vue, React, and Svelte adapters are `@sticky-aside/vue`, `@sticky-aside/react`, and `@sticky-aside/svelte`. The full guide is in the [repository README](https://github.com/maprox/sticky-aside#readme).
@@ -0,0 +1,10 @@
1
+ import { _ as StickyFrame, b as StickyReason, c as StickyAsideView, d as StickyPaintFlow, f as StickyPaintLocked, g as Box, h as StickyReading, i as StickyAsideHandlers, l as StickyPaint, m as StickyPaintTop, o as StickyAsidePresenterOptions, p as StickyPaintOff, r as StickyAsideController, t as ScrollMetrics, u as StickyPaintBottom, v as StickyMode, y as StickyModel } from "./types.js";
2
+ //#region src/presenter/presenter.d.ts
3
+ export declare function createStickyAsidePresenter(view: StickyAsideView, options: StickyAsidePresenterOptions): StickyAsideController;
4
+ //#endregion
5
+ //#region src/model/step.d.ts
6
+ export declare function getInitialStickyModel(): StickyModel;
7
+ export declare function getNewStickyModel(model: StickyModel, frame: StickyFrame): StickyModel;
8
+ //#endregion
9
+ export type { Box, ScrollMetrics, StickyAsideHandlers, StickyAsideView, StickyFrame, StickyMode, StickyModel, StickyPaint, StickyPaintBottom, StickyPaintFlow, StickyPaintLocked, StickyPaintOff, StickyPaintTop, StickyReading, StickyReason };
10
+ //# sourceMappingURL=headless.d.ts.map
@@ -0,0 +1,3 @@
1
+ import { n as getInitialStickyModel, r as getNewStickyModel, t as createStickyAsidePresenter } from "./presenter.js";
2
+
3
+ export { createStickyAsidePresenter, getInitialStickyModel, getNewStickyModel };
@@ -0,0 +1,12 @@
1
+ import { a as StickyAsideOptions, g as Box, n as StickyAsideClasses, o as StickyAsidePresenterOptions, r as StickyAsideController, s as StickyAsideState, t as ScrollMetrics } from "./types.js";
2
+ //#region src/view/constants.d.ts
3
+ export declare const OFFSET_CLASS: {
4
+ readonly top: "sticky-aside-top";
5
+ readonly bottom: "sticky-aside-bottom";
6
+ };
7
+ //#endregion
8
+ //#region src/create-sticky-aside.d.ts
9
+ export declare function createStickyAside(options: StickyAsideOptions): StickyAsideController;
10
+ //#endregion
11
+ export type { Box, ScrollMetrics, StickyAsideClasses, StickyAsideController, StickyAsideOptions, StickyAsidePresenterOptions, StickyAsideState };
12
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,164 @@
1
+ import { t as createStickyAsidePresenter } from "./presenter.js";
2
+
3
+ //#region src/view/constants.ts
4
+ const OFFSET_CLASS = {
5
+ top: "sticky-aside-top",
6
+ bottom: "sticky-aside-bottom"
7
+ };
8
+ const DATASET = {
9
+ measure: "stickyMeasure",
10
+ aside: "stickyAside"
11
+ };
12
+ const ASIDE_STATE = {
13
+ off: "off",
14
+ flow: "flow",
15
+ bottom: "bottom",
16
+ locked: "locked",
17
+ top: "top"
18
+ };
19
+ const MEASURE_STYLE = {
20
+ position: "fixed",
21
+ visibility: "hidden",
22
+ pointerEvents: "none",
23
+ left: "0",
24
+ width: "0"
25
+ };
26
+ const WRAPPER_STYLE = {
27
+ position: "relative",
28
+ height: "100%"
29
+ };
30
+
31
+ //#endregion
32
+ //#region src/view/browser-view.ts
33
+ function createBrowserView(options) {
34
+ const measure = document.createElement("div");
35
+ measure.dataset[DATASET.measure] = "";
36
+ Object.assign(measure.style, MEASURE_STYLE);
37
+ options.wrapper.append(measure);
38
+ Object.assign(options.wrapper.style, WRAPPER_STYLE);
39
+ let resizeObserver;
40
+ let onScroll;
41
+ let onResize;
42
+ let topClass = options.topClass ?? OFFSET_CLASS.top;
43
+ let bottomClass = options.bottomClass ?? OFFSET_CLASS.bottom;
44
+ let contentClass = options.contentClass ?? "";
45
+ let appliedMeasure = [];
46
+ let appliedContent = [];
47
+ function getClassTokens(value) {
48
+ return value.split(/\s+/).filter((token) => token.length > 0);
49
+ }
50
+ function syncClassTokens(element, previous, next) {
51
+ for (const token of previous) if (!next.includes(token)) element.classList.remove(token);
52
+ for (const token of next) element.classList.add(token);
53
+ return next;
54
+ }
55
+ function clearContentStyle() {
56
+ options.content.style.position = "";
57
+ options.content.style.top = "";
58
+ options.content.style.bottom = "";
59
+ options.content.style.width = "";
60
+ }
61
+ function getBox(element) {
62
+ if (options.readRect) return options.readRect(element);
63
+ const rect = element.getBoundingClientRect();
64
+ return {
65
+ top: rect.top,
66
+ bottom: rect.bottom,
67
+ width: rect.width,
68
+ height: rect.height
69
+ };
70
+ }
71
+ function getMetrics() {
72
+ if (options.getScrollMetrics) return options.getScrollMetrics();
73
+ return {
74
+ scrollY: window.scrollY,
75
+ viewportHeight: window.innerHeight,
76
+ documentHeight: document.body.clientHeight
77
+ };
78
+ }
79
+ function read() {
80
+ return {
81
+ metrics: getMetrics(),
82
+ wrapper: getBox(options.wrapper),
83
+ content: getBox(options.content),
84
+ measure: getBox(measure)
85
+ };
86
+ }
87
+ function setClasses(next) {
88
+ if (next.topClass !== void 0) topClass = next.topClass;
89
+ if (next.bottomClass !== void 0) bottomClass = next.bottomClass;
90
+ if (next.contentClass !== void 0) contentClass = next.contentClass;
91
+ }
92
+ function paintMeasure() {
93
+ const contentBox = getBox(options.content);
94
+ measure.style.top = "";
95
+ measure.style.bottom = "";
96
+ measure.style.maxHeight = `${contentBox.height}px`;
97
+ appliedMeasure = syncClassTokens(measure, appliedMeasure, [...getClassTokens(topClass), ...getClassTokens(bottomClass)]);
98
+ }
99
+ function paint(next) {
100
+ clearContentStyle();
101
+ options.content.dataset[DATASET.aside] = ASIDE_STATE[next.state];
102
+ const nextTokens = getClassTokens(contentClass);
103
+ if (next.state === "off") options.content.style.position = "relative";
104
+ else if (next.state === "flow") {
105
+ options.content.style.position = "relative";
106
+ options.content.style.top = `${next.top}px`;
107
+ } else if (next.state === "bottom") {
108
+ options.content.style.position = "fixed";
109
+ options.content.style.width = `${next.width}px`;
110
+ nextTokens.push(...getClassTokens(bottomClass));
111
+ } else if (next.state === "locked") {
112
+ options.content.style.position = "fixed";
113
+ options.content.style.width = `${next.width}px`;
114
+ options.content.style.top = `${next.top}px`;
115
+ } else {
116
+ options.content.style.position = "fixed";
117
+ options.content.style.width = `${next.width}px`;
118
+ nextTokens.push(...getClassTokens(topClass));
119
+ }
120
+ appliedContent = syncClassTokens(options.content, appliedContent, nextTokens);
121
+ }
122
+ function unsubscribe() {
123
+ resizeObserver?.disconnect();
124
+ resizeObserver = void 0;
125
+ if (onScroll) window.removeEventListener("scroll", onScroll);
126
+ if (onResize) window.removeEventListener("resize", onResize);
127
+ onScroll = void 0;
128
+ onResize = void 0;
129
+ }
130
+ function subscribe(handlers) {
131
+ onScroll = () => handlers.scroll();
132
+ onResize = () => handlers.resize();
133
+ resizeObserver = new ResizeObserver(() => handlers.resize());
134
+ resizeObserver.observe(options.content);
135
+ resizeObserver.observe(measure);
136
+ window.addEventListener("scroll", onScroll, { passive: true });
137
+ window.addEventListener("resize", onResize, { passive: true });
138
+ return unsubscribe;
139
+ }
140
+ function destroy() {
141
+ appliedContent = syncClassTokens(options.content, appliedContent, []);
142
+ appliedMeasure = syncClassTokens(measure, appliedMeasure, []);
143
+ measure.remove();
144
+ clearContentStyle();
145
+ }
146
+ return {
147
+ read,
148
+ setClasses,
149
+ paintMeasure,
150
+ paint,
151
+ subscribe,
152
+ destroy
153
+ };
154
+ }
155
+
156
+ //#endregion
157
+ //#region src/create-sticky-aside.ts
158
+ function createStickyAside(options) {
159
+ return createStickyAsidePresenter(createBrowserView(options), options);
160
+ }
161
+
162
+ //#endregion
163
+ export { OFFSET_CLASS, createStickyAside };
164
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/view/constants.ts","../src/view/browser-view.ts","../src/create-sticky-aside.ts"],"sourcesContent":["export const OFFSET_CLASS = {\n top: 'sticky-aside-top',\n bottom: 'sticky-aside-bottom',\n} as const\n\nexport const DATASET = {\n measure: 'stickyMeasure',\n aside: 'stickyAside',\n} as const\n\nexport const ASIDE_STATE = {\n off: 'off',\n flow: 'flow',\n bottom: 'bottom',\n locked: 'locked',\n top: 'top',\n} as const\n\nexport const MEASURE_STYLE = {\n position: 'fixed',\n visibility: 'hidden',\n pointerEvents: 'none',\n left: '0',\n width: '0',\n} as const\n\nexport const WRAPPER_STYLE = {\n position: 'relative',\n height: '100%',\n} as const\n","import type { Box } from '#core/model/types.ts'\nimport type {\n ScrollMetrics,\n StickyAsideClasses,\n StickyAsideHandlers,\n StickyAsideOptions,\n StickyAsideView,\n StickyPaint,\n StickyReading,\n} from '#core/types.ts'\nimport { ASIDE_STATE, DATASET, MEASURE_STYLE, OFFSET_CLASS, WRAPPER_STYLE } from '#core/view/constants.ts'\n\nexport function createBrowserView(options: StickyAsideOptions): StickyAsideView {\n const measure = document.createElement('div')\n measure.dataset[DATASET.measure] = ''\n Object.assign(measure.style, MEASURE_STYLE)\n options.wrapper.append(measure)\n Object.assign(options.wrapper.style, WRAPPER_STYLE)\n\n let resizeObserver: ResizeObserver | undefined\n let onScroll: (() => void) | undefined\n let onResize: (() => void) | undefined\n let topClass = options.topClass ?? OFFSET_CLASS.top\n let bottomClass = options.bottomClass ?? OFFSET_CLASS.bottom\n let contentClass = options.contentClass ?? ''\n let appliedMeasure: string[] = []\n let appliedContent: string[] = []\n\n function getClassTokens(value: string): string[] {\n return value.split(/\\s+/).filter((token) => token.length > 0)\n }\n\n function syncClassTokens(element: HTMLElement, previous: string[], next: string[]): string[] {\n for (const token of previous) {\n if (!next.includes(token)) {\n element.classList.remove(token)\n }\n }\n\n for (const token of next) {\n element.classList.add(token)\n }\n\n return next\n }\n\n function clearContentStyle() {\n options.content.style.position = ''\n options.content.style.top = ''\n options.content.style.bottom = ''\n options.content.style.width = ''\n }\n\n function getBox(element: HTMLElement): Box {\n if (options.readRect) {\n return options.readRect(element)\n }\n\n const rect = element.getBoundingClientRect()\n\n return {\n top: rect.top,\n bottom: rect.bottom,\n width: rect.width,\n height: rect.height,\n }\n }\n\n function getMetrics(): ScrollMetrics {\n if (options.getScrollMetrics) {\n return options.getScrollMetrics()\n }\n\n return {\n scrollY: window.scrollY,\n viewportHeight: window.innerHeight,\n documentHeight: document.body.clientHeight,\n }\n }\n\n function read(): StickyReading {\n return {\n metrics: getMetrics(),\n wrapper: getBox(options.wrapper),\n content: getBox(options.content),\n measure: getBox(measure),\n }\n }\n\n function setClasses(next: StickyAsideClasses) {\n if (next.topClass !== undefined) {\n topClass = next.topClass\n }\n\n if (next.bottomClass !== undefined) {\n bottomClass = next.bottomClass\n }\n\n if (next.contentClass !== undefined) {\n contentClass = next.contentClass\n }\n }\n\n function paintMeasure() {\n const contentBox = getBox(options.content)\n measure.style.top = ''\n measure.style.bottom = ''\n measure.style.maxHeight = `${contentBox.height}px`\n appliedMeasure = syncClassTokens(measure, appliedMeasure, [\n ...getClassTokens(topClass),\n ...getClassTokens(bottomClass),\n ])\n }\n\n function paint(next: StickyPaint) {\n clearContentStyle()\n options.content.dataset[DATASET.aside] = ASIDE_STATE[next.state]\n const nextTokens = getClassTokens(contentClass)\n if (next.state === 'off') {\n options.content.style.position = 'relative'\n } else if (next.state === 'flow') {\n options.content.style.position = 'relative'\n options.content.style.top = `${next.top}px`\n } else if (next.state === 'bottom') {\n options.content.style.position = 'fixed'\n options.content.style.width = `${next.width}px`\n nextTokens.push(...getClassTokens(bottomClass))\n } else if (next.state === 'locked') {\n options.content.style.position = 'fixed'\n options.content.style.width = `${next.width}px`\n options.content.style.top = `${next.top}px`\n } else {\n options.content.style.position = 'fixed'\n options.content.style.width = `${next.width}px`\n nextTokens.push(...getClassTokens(topClass))\n }\n\n appliedContent = syncClassTokens(options.content, appliedContent, nextTokens)\n }\n\n function unsubscribe() {\n resizeObserver?.disconnect()\n resizeObserver = undefined\n if (onScroll) {\n window.removeEventListener('scroll', onScroll)\n }\n\n if (onResize) {\n window.removeEventListener('resize', onResize)\n }\n\n onScroll = undefined\n onResize = undefined\n }\n\n function subscribe(handlers: StickyAsideHandlers) {\n onScroll = () => handlers.scroll()\n onResize = () => handlers.resize()\n resizeObserver = new ResizeObserver(() => handlers.resize())\n resizeObserver.observe(options.content)\n resizeObserver.observe(measure)\n window.addEventListener('scroll', onScroll, {\n passive: true,\n })\n window.addEventListener('resize', onResize, {\n passive: true,\n })\n\n return unsubscribe\n }\n\n function destroy() {\n appliedContent = syncClassTokens(options.content, appliedContent, [])\n appliedMeasure = syncClassTokens(measure, appliedMeasure, [])\n measure.remove()\n clearContentStyle()\n }\n\n return {\n read,\n setClasses,\n paintMeasure,\n paint,\n subscribe,\n destroy,\n }\n}\n","import { createStickyAsidePresenter } from '#core/presenter/presenter.ts'\nimport type { StickyAsideController, StickyAsideOptions } from '#core/types.ts'\nimport { createBrowserView } from '#core/view/browser-view.ts'\n\nexport function createStickyAside(options: StickyAsideOptions): StickyAsideController {\n return createStickyAsidePresenter(createBrowserView(options), options)\n}\n"],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ .sticky-aside-top {
2
+ top: var(--sticky-aside-top, 0px);
3
+ }
4
+
5
+ .sticky-aside-bottom {
6
+ bottom: var(--sticky-aside-bottom, 0px);
7
+ }
@@ -0,0 +1,296 @@
1
+ //#region src/model/constants.ts
2
+ const MODE = {
3
+ relative: "relative",
4
+ fixedTop: "fixed-top",
5
+ fixedBottom: "fixed-bottom",
6
+ fixedLocked: "fixed-locked"
7
+ };
8
+ const REASON = {
9
+ scroll: "scroll",
10
+ resize: "resize",
11
+ lock: "lock",
12
+ unlock: "unlock"
13
+ };
14
+
15
+ //#endregion
16
+ //#region src/model/utils.ts
17
+ function getClearedLockModel(model) {
18
+ return {
19
+ ...model,
20
+ locked: false,
21
+ lockedTop: null
22
+ };
23
+ }
24
+ function getPinnedStickyModel(model, edge) {
25
+ return {
26
+ ...model,
27
+ mode: edge === "top" ? MODE.fixedTop : MODE.fixedBottom,
28
+ locked: false
29
+ };
30
+ }
31
+ function getLockedStickyModel({ model, frame }) {
32
+ const lockedTop = model.lockedTop ?? frame.content.top;
33
+ const isPastTop = !frame.allowLockedOverflowTop && frame.wrapper.top > lockedTop;
34
+ const isPastBottom = !frame.allowLockedOverflowBottom && lockedTop + frame.content.height > frame.wrapper.top + frame.wrapper.height;
35
+ if (isPastTop || isPastBottom) return getRelativeStickyModel({
36
+ model: {
37
+ ...model,
38
+ locked: true,
39
+ lockedTop
40
+ },
41
+ frame
42
+ });
43
+ return {
44
+ ...model,
45
+ mode: MODE.fixedLocked,
46
+ locked: true,
47
+ lockedTop
48
+ };
49
+ }
50
+ function getRelativeStickyModel({ model, frame }) {
51
+ let containerTop = Math.max(frame.content.top - frame.wrapper.top, 0);
52
+ if (!allowsBottomGap(frame)) containerTop = getClampedContainerTop(containerTop, frame);
53
+ return {
54
+ mode: MODE.relative,
55
+ containerTop,
56
+ locked: model.locked,
57
+ lockedTop: model.locked ? model.lockedTop : null
58
+ };
59
+ }
60
+ function getStickyModelAtEdges({ model, frame }) {
61
+ const isMeasureBelow = frame.measure.bottom > frame.wrapper.bottom;
62
+ const isAlreadyAtBottom = Math.round(frame.content.bottom) === Math.round(frame.wrapper.bottom);
63
+ const isAlreadyAtTop = frame.content.top === 0;
64
+ if (isMeasureBelow && isAlreadyAtBottom || !isMeasureBelow && isAlreadyAtTop) return model;
65
+ const containerTop = getClampedContainerTop(isMeasureBelow ? frame.wrapper.height : 0, frame);
66
+ return {
67
+ mode: MODE.relative,
68
+ containerTop,
69
+ locked: false,
70
+ lockedTop: null
71
+ };
72
+ }
73
+ function isFullyVisible(frame) {
74
+ return frame.measure.height >= frame.content.height;
75
+ }
76
+ function isMeasureInside(frame) {
77
+ return frame.measure.top >= frame.wrapper.top && frame.measure.bottom <= frame.wrapper.bottom;
78
+ }
79
+ function isOutsideViewport(frame) {
80
+ return frame.scrollY < 0 || frame.scrollY > frame.documentHeight - frame.viewportHeight;
81
+ }
82
+ function isFixed(model) {
83
+ return model.mode !== MODE.relative;
84
+ }
85
+ function isScrollingOpposite({ model, frame }) {
86
+ const isFixedTop = model.mode === MODE.fixedTop || model.mode === MODE.fixedLocked;
87
+ return frame.scrollingUp && !isFixedTop || !frame.scrollingUp && isFixedTop;
88
+ }
89
+ function allowsBottomGap(frame) {
90
+ return frame.reason === REASON.unlock && frame.allowLockedOverflowBottom;
91
+ }
92
+ function getClampedContainerTop(containerTop, frame) {
93
+ const bottomGap = frame.wrapper.height - (containerTop + frame.content.height);
94
+ let next = containerTop;
95
+ if (bottomGap < 0) next += bottomGap;
96
+ return next < 0 ? 0 : next;
97
+ }
98
+
99
+ //#endregion
100
+ //#region src/model/step.ts
101
+ function getInitialStickyModel() {
102
+ return {
103
+ mode: MODE.relative,
104
+ containerTop: 0,
105
+ locked: false,
106
+ lockedTop: null
107
+ };
108
+ }
109
+ function getNewStickyModel(model, frame) {
110
+ if (frame.disabled) return model;
111
+ if (frame.reason === REASON.unlock) return getRelativeStickyModel({
112
+ model: getClearedLockModel(model),
113
+ frame
114
+ });
115
+ if (frame.reason === REASON.lock) return getLockedStickyModel({
116
+ model: {
117
+ ...model,
118
+ locked: true,
119
+ lockedTop: frame.content.top
120
+ },
121
+ frame
122
+ });
123
+ if (frame.locked) return getLockedStickyModel({
124
+ model,
125
+ frame
126
+ });
127
+ if (frame.reason === REASON.resize) {
128
+ if (isMeasureInside(frame)) return isFullyVisible(frame) ? getPinnedStickyModel(model, "top") : model;
129
+ return getRelativeStickyModel({
130
+ model: getClearedLockModel(model),
131
+ frame
132
+ });
133
+ }
134
+ if (isFixed(model)) return !isMeasureInside(frame) && !isOutsideViewport(frame) || !isFullyVisible(frame) && isScrollingOpposite({
135
+ model,
136
+ frame
137
+ }) ? getRelativeStickyModel({
138
+ model: getClearedLockModel(model),
139
+ frame
140
+ }) : model;
141
+ if (isMeasureInside(frame)) {
142
+ if (isFullyVisible(frame)) return getPinnedStickyModel(model, "top");
143
+ if (frame.scrollingUp && frame.content.top > frame.measure.top) return getPinnedStickyModel(model, "top");
144
+ if (!frame.scrollingUp && frame.content.bottom < frame.measure.bottom) return getPinnedStickyModel(model, "bottom");
145
+ return model;
146
+ }
147
+ return getStickyModelAtEdges({
148
+ model,
149
+ frame
150
+ });
151
+ }
152
+
153
+ //#endregion
154
+ //#region src/presenter/constants.ts
155
+ const DEFAULTS = {
156
+ disabled: false,
157
+ locked: false,
158
+ allowLockedOverflowTop: false,
159
+ allowLockedOverflowBottom: false
160
+ };
161
+
162
+ //#endregion
163
+ //#region src/presenter/utils.ts
164
+ function getSession(options) {
165
+ return {
166
+ disabled: options.disabled ?? DEFAULTS.disabled,
167
+ locked: options.locked ?? DEFAULTS.locked,
168
+ allowLockedOverflowTop: options.allowLockedOverflowTop ?? DEFAULTS.allowLockedOverflowTop,
169
+ allowLockedOverflowBottom: options.allowLockedOverflowBottom ?? DEFAULTS.allowLockedOverflowBottom
170
+ };
171
+ }
172
+ function getStickyFrame(session, reading, isScrollingUp, reason) {
173
+ return {
174
+ disabled: session.disabled,
175
+ locked: session.locked,
176
+ allowLockedOverflowTop: session.allowLockedOverflowTop,
177
+ allowLockedOverflowBottom: session.allowLockedOverflowBottom,
178
+ scrollingUp: isScrollingUp,
179
+ scrollY: reading.metrics.scrollY,
180
+ viewportHeight: reading.metrics.viewportHeight,
181
+ documentHeight: reading.metrics.documentHeight,
182
+ wrapper: reading.wrapper,
183
+ content: reading.content,
184
+ measure: reading.measure,
185
+ reason
186
+ };
187
+ }
188
+ function getStickyPaint(model, session, wrapperWidth) {
189
+ if (session.disabled) return { state: "off" };
190
+ if (model.mode === MODE.relative) return {
191
+ state: "flow",
192
+ top: model.containerTop
193
+ };
194
+ if (model.mode === MODE.fixedBottom) return {
195
+ state: "bottom",
196
+ width: wrapperWidth
197
+ };
198
+ if (model.mode === MODE.fixedLocked) return {
199
+ state: "locked",
200
+ top: model.lockedTop ?? 0,
201
+ width: wrapperWidth
202
+ };
203
+ return {
204
+ state: "top",
205
+ width: wrapperWidth
206
+ };
207
+ }
208
+
209
+ //#endregion
210
+ //#region src/presenter/presenter.ts
211
+ function createStickyAsidePresenter(view, options) {
212
+ const session = getSession(options);
213
+ let model = getInitialStickyModel();
214
+ let isScrollingUp = false;
215
+ let isStopped = false;
216
+ let painted;
217
+ let lastScrollY = view.read().metrics.scrollY;
218
+ function report(state) {
219
+ if (isStopped || state === painted) return;
220
+ painted = state;
221
+ options.onStateChange?.(state);
222
+ }
223
+ function apply(reason) {
224
+ if (isStopped) return;
225
+ view.paintMeasure();
226
+ const reading = view.read();
227
+ model = getNewStickyModel(model, getStickyFrame(session, reading, isScrollingUp, reason));
228
+ const paint = getStickyPaint(model, session, reading.wrapper.width);
229
+ view.paint(paint);
230
+ report(paint.state);
231
+ }
232
+ function onScroll() {
233
+ const scrollY = view.read().metrics.scrollY;
234
+ isScrollingUp = scrollY < lastScrollY;
235
+ lastScrollY = scrollY;
236
+ apply(REASON.scroll);
237
+ }
238
+ function onResize() {
239
+ apply(REASON.resize);
240
+ }
241
+ function update(reason = REASON.resize) {
242
+ apply(reason);
243
+ }
244
+ function setLocked(isLocked) {
245
+ if (isStopped || isLocked === session.locked) return;
246
+ session.locked = isLocked;
247
+ apply(isLocked ? REASON.lock : REASON.unlock);
248
+ }
249
+ function setDisabled(isDisabled) {
250
+ if (isStopped || isDisabled === session.disabled) return;
251
+ session.disabled = isDisabled;
252
+ apply(REASON.resize);
253
+ }
254
+ function setClasses(next) {
255
+ if (isStopped) return;
256
+ view.setClasses(next);
257
+ apply(REASON.resize);
258
+ }
259
+ function setAllowLockedOverflow(isAllowTop, isAllowBottom) {
260
+ if (isStopped) return;
261
+ session.allowLockedOverflowTop = isAllowTop;
262
+ session.allowLockedOverflowBottom = isAllowBottom;
263
+ apply(REASON.resize);
264
+ }
265
+ function getState() {
266
+ return painted ?? "off";
267
+ }
268
+ function getModel() {
269
+ return model;
270
+ }
271
+ const unsubscribe = view.subscribe({
272
+ scroll: onScroll,
273
+ resize: onResize
274
+ });
275
+ function destroy() {
276
+ if (isStopped) return;
277
+ isStopped = true;
278
+ unsubscribe();
279
+ view.destroy();
280
+ }
281
+ apply(session.locked ? REASON.lock : REASON.resize);
282
+ return {
283
+ update,
284
+ setLocked,
285
+ setDisabled,
286
+ setClasses,
287
+ setAllowLockedOverflow,
288
+ destroy,
289
+ getState,
290
+ getModel
291
+ };
292
+ }
293
+
294
+ //#endregion
295
+ export { getInitialStickyModel as n, getNewStickyModel as r, createStickyAsidePresenter as t };
296
+ //# sourceMappingURL=presenter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"presenter.js","names":[],"sources":["../src/model/constants.ts","../src/model/utils.ts","../src/model/step.ts","../src/presenter/constants.ts","../src/presenter/utils.ts","../src/presenter/presenter.ts"],"sourcesContent":["export const MODE = {\n relative: 'relative',\n fixedTop: 'fixed-top',\n fixedBottom: 'fixed-bottom',\n fixedLocked: 'fixed-locked',\n} as const\n\nexport const REASON = {\n scroll: 'scroll',\n resize: 'resize',\n lock: 'lock',\n unlock: 'unlock',\n} as const\n","import { MODE, REASON } from '#core/model/constants.ts'\nimport type { StickyFrame, StickyModel } from '#core/model/types.ts'\n\nexport interface StickyStep {\n model: StickyModel\n frame: StickyFrame\n}\n\nexport function getClearedLockModel(model: StickyModel): StickyModel {\n return {\n ...model,\n locked: false,\n lockedTop: null,\n }\n}\n\nexport function getPinnedStickyModel(model: StickyModel, edge: 'top' | 'bottom'): StickyModel {\n return {\n ...model,\n mode: edge === 'top' ? MODE.fixedTop : MODE.fixedBottom,\n locked: false,\n }\n}\n\nexport function getLockedStickyModel({ model, frame }: StickyStep): StickyModel {\n const lockedTop = model.lockedTop ?? frame.content.top\n const isPastTop = !frame.allowLockedOverflowTop && frame.wrapper.top > lockedTop\n const isPastBottom = !frame.allowLockedOverflowBottom &&\n lockedTop + frame.content.height > frame.wrapper.top + frame.wrapper.height\n\n if (isPastTop || isPastBottom) {\n return getRelativeStickyModel({\n model: {\n ...model,\n locked: true,\n lockedTop,\n },\n frame,\n })\n }\n\n return {\n ...model,\n mode: MODE.fixedLocked,\n locked: true,\n lockedTop,\n }\n}\n\nexport function getRelativeStickyModel({ model, frame }: StickyStep): StickyModel {\n let containerTop = Math.max(frame.content.top - frame.wrapper.top, 0)\n if (!allowsBottomGap(frame)) {\n containerTop = getClampedContainerTop(containerTop, frame)\n }\n\n return {\n mode: MODE.relative,\n containerTop,\n locked: model.locked,\n lockedTop: model.locked ? model.lockedTop : null,\n }\n}\n\nexport function getStickyModelAtEdges({ model, frame }: StickyStep): StickyModel {\n const isMeasureBelow = frame.measure.bottom > frame.wrapper.bottom\n const isAlreadyAtBottom = Math.round(frame.content.bottom) === Math.round(frame.wrapper.bottom)\n const isAlreadyAtTop = frame.content.top === 0\n\n if ((isMeasureBelow && isAlreadyAtBottom) || (!isMeasureBelow && isAlreadyAtTop)) {\n return model\n }\n\n const containerTop = getClampedContainerTop(isMeasureBelow ? frame.wrapper.height : 0, frame)\n\n return {\n mode: MODE.relative,\n containerTop,\n locked: false,\n lockedTop: null,\n }\n}\n\nexport function isFullyVisible(frame: StickyFrame): boolean {\n return frame.measure.height >= frame.content.height\n}\n\nexport function isMeasureInside(frame: StickyFrame): boolean {\n return frame.measure.top >= frame.wrapper.top && frame.measure.bottom <= frame.wrapper.bottom\n}\n\nexport function isOutsideViewport(frame: StickyFrame): boolean {\n return frame.scrollY < 0 || frame.scrollY > frame.documentHeight - frame.viewportHeight\n}\n\nexport function isFixed(model: StickyModel): boolean {\n return model.mode !== MODE.relative\n}\n\nexport function isScrollingOpposite({ model, frame }: StickyStep): boolean {\n const isFixedTop = model.mode === MODE.fixedTop || model.mode === MODE.fixedLocked\n\n return (frame.scrollingUp && !isFixedTop) || (!frame.scrollingUp && isFixedTop)\n}\n\nfunction allowsBottomGap(frame: StickyFrame): boolean {\n return frame.reason === REASON.unlock && frame.allowLockedOverflowBottom\n}\n\nfunction getClampedContainerTop(containerTop: number, frame: StickyFrame): number {\n const bottomGap = frame.wrapper.height - (containerTop + frame.content.height)\n let next = containerTop\n if (bottomGap < 0) {\n next += bottomGap\n }\n\n return next < 0 ? 0 : next\n}\n","import { MODE, REASON } from '#core/model/constants.ts'\nimport type { Box, StickyFrame, StickyModel } from '#core/model/types.ts'\nimport {\n getClearedLockModel,\n getLockedStickyModel,\n getPinnedStickyModel,\n getRelativeStickyModel,\n getStickyModelAtEdges,\n isFixed,\n isFullyVisible,\n isMeasureInside,\n isOutsideViewport,\n isScrollingOpposite,\n} from '#core/model/utils.ts'\n\nexport type { Box, StickyFrame, StickyModel }\n\nexport function getInitialStickyModel(): StickyModel {\n return {\n mode: MODE.relative,\n containerTop: 0,\n locked: false,\n lockedTop: null,\n }\n}\n\nexport function getNewStickyModel(model: StickyModel, frame: StickyFrame): StickyModel {\n if (frame.disabled) {\n return model\n }\n\n if (frame.reason === REASON.unlock) {\n return getRelativeStickyModel({ model: getClearedLockModel(model), frame })\n }\n\n if (frame.reason === REASON.lock) {\n return getLockedStickyModel({\n model: {\n ...model,\n locked: true,\n lockedTop: frame.content.top,\n },\n frame,\n })\n }\n\n if (frame.locked) {\n return getLockedStickyModel({ model, frame })\n }\n\n if (frame.reason === REASON.resize) {\n if (isMeasureInside(frame)) {\n return isFullyVisible(frame) ? getPinnedStickyModel(model, 'top') : model\n }\n\n return getRelativeStickyModel({ model: getClearedLockModel(model), frame })\n }\n\n if (isFixed(model)) {\n const isReleasing =\n (!isMeasureInside(frame) && !isOutsideViewport(frame)) ||\n (!isFullyVisible(frame) && isScrollingOpposite({ model, frame }))\n\n return isReleasing ? getRelativeStickyModel({ model: getClearedLockModel(model), frame }) : model\n }\n\n if (isMeasureInside(frame)) {\n if (isFullyVisible(frame)) {\n return getPinnedStickyModel(model, 'top')\n }\n\n if (frame.scrollingUp && frame.content.top > frame.measure.top) {\n return getPinnedStickyModel(model, 'top')\n }\n\n if (!frame.scrollingUp && frame.content.bottom < frame.measure.bottom) {\n return getPinnedStickyModel(model, 'bottom')\n }\n\n return model\n }\n\n return getStickyModelAtEdges({ model, frame })\n}\n","export const DEFAULTS = {\n disabled: false,\n locked: false,\n allowLockedOverflowTop: false,\n allowLockedOverflowBottom: false,\n} as const\n","import { MODE } from '#core/model/constants.ts'\nimport type { StickyFrame, StickyModel, StickyReason } from '#core/model/types.ts'\nimport { DEFAULTS } from '#core/presenter/constants.ts'\nimport type { Session } from '#core/presenter/types.ts'\nimport type { StickyAsidePresenterOptions, StickyPaint, StickyReading } from '#core/types.ts'\n\nexport function getSession(options: StickyAsidePresenterOptions): Session {\n return {\n disabled: options.disabled ?? DEFAULTS.disabled,\n locked: options.locked ?? DEFAULTS.locked,\n allowLockedOverflowTop: options.allowLockedOverflowTop ?? DEFAULTS.allowLockedOverflowTop,\n allowLockedOverflowBottom: options.allowLockedOverflowBottom ?? DEFAULTS.allowLockedOverflowBottom,\n }\n}\n\nexport function getStickyFrame(\n session: Session,\n reading: StickyReading,\n isScrollingUp: boolean,\n reason: StickyReason,\n): StickyFrame {\n return {\n disabled: session.disabled,\n locked: session.locked,\n allowLockedOverflowTop: session.allowLockedOverflowTop,\n allowLockedOverflowBottom: session.allowLockedOverflowBottom,\n scrollingUp: isScrollingUp,\n scrollY: reading.metrics.scrollY,\n viewportHeight: reading.metrics.viewportHeight,\n documentHeight: reading.metrics.documentHeight,\n wrapper: reading.wrapper,\n content: reading.content,\n measure: reading.measure,\n reason,\n }\n}\n\nexport function getStickyPaint(model: StickyModel, session: Session, wrapperWidth: number): StickyPaint {\n if (session.disabled) {\n return {\n state: 'off',\n }\n }\n\n if (model.mode === MODE.relative) {\n return {\n state: 'flow',\n top: model.containerTop,\n }\n }\n\n if (model.mode === MODE.fixedBottom) {\n return {\n state: 'bottom',\n width: wrapperWidth,\n }\n }\n\n if (model.mode === MODE.fixedLocked) {\n return {\n state: 'locked',\n top: model.lockedTop ?? 0,\n width: wrapperWidth,\n }\n }\n\n return {\n state: 'top',\n width: wrapperWidth,\n }\n}\n","import { REASON } from '#core/model/constants.ts'\nimport { getInitialStickyModel, getNewStickyModel } from '#core/model/step.ts'\nimport type { StickyReason } from '#core/model/types.ts'\nimport { getSession, getStickyFrame, getStickyPaint } from '#core/presenter/utils.ts'\nimport type {\n StickyAsideClasses,\n StickyAsideController,\n StickyAsidePresenterOptions,\n StickyAsideState,\n StickyAsideView,\n} from '#core/types.ts'\n\nexport function createStickyAsidePresenter(\n view: StickyAsideView,\n options: StickyAsidePresenterOptions,\n): StickyAsideController {\n const session = getSession(options)\n let model = getInitialStickyModel()\n let isScrollingUp = false\n let isStopped = false\n let painted: StickyAsideState | undefined\n let lastScrollY = view.read().metrics.scrollY\n\n function report(state: StickyAsideState) {\n if (isStopped || state === painted) {\n return\n }\n\n painted = state\n options.onStateChange?.(state)\n }\n\n function apply(reason: StickyReason) {\n if (isStopped) {\n return\n }\n\n view.paintMeasure()\n const reading = view.read()\n model = getNewStickyModel(model, getStickyFrame(session, reading, isScrollingUp, reason))\n const paint = getStickyPaint(model, session, reading.wrapper.width)\n view.paint(paint)\n report(paint.state)\n }\n\n function onScroll() {\n const scrollY = view.read().metrics.scrollY\n isScrollingUp = scrollY < lastScrollY\n lastScrollY = scrollY\n apply(REASON.scroll)\n }\n\n function onResize() {\n apply(REASON.resize)\n }\n\n function update(reason: Extract<StickyReason, 'scroll' | 'resize'> = REASON.resize) {\n apply(reason)\n }\n\n function setLocked(isLocked: boolean) {\n if (isStopped || isLocked === session.locked) {\n return\n }\n\n session.locked = isLocked\n apply(isLocked ? REASON.lock : REASON.unlock)\n }\n\n function setDisabled(isDisabled: boolean) {\n if (isStopped || isDisabled === session.disabled) {\n return\n }\n\n session.disabled = isDisabled\n apply(REASON.resize)\n }\n\n function setClasses(next: StickyAsideClasses) {\n if (isStopped) {\n return\n }\n\n view.setClasses(next)\n apply(REASON.resize)\n }\n\n function setAllowLockedOverflow(isAllowTop: boolean, isAllowBottom: boolean) {\n if (isStopped) {\n return\n }\n\n session.allowLockedOverflowTop = isAllowTop\n session.allowLockedOverflowBottom = isAllowBottom\n apply(REASON.resize)\n }\n\n function getState() {\n return painted ?? 'off'\n }\n\n function getModel() {\n return model\n }\n\n const unsubscribe = view.subscribe({\n scroll: onScroll,\n resize: onResize,\n })\n\n function destroy() {\n if (isStopped) {\n return\n }\n\n isStopped = true\n unsubscribe()\n view.destroy()\n }\n\n apply(session.locked ? REASON.lock : REASON.resize)\n\n return {\n update,\n setLocked,\n setDisabled,\n setClasses,\n setAllowLockedOverflow,\n destroy,\n getState,\n getModel,\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,124 @@
1
+ //#region src/model/constants.d.ts
2
+ declare const MODE: {
3
+ readonly relative: "relative";
4
+ readonly fixedTop: "fixed-top";
5
+ readonly fixedBottom: "fixed-bottom";
6
+ readonly fixedLocked: "fixed-locked";
7
+ };
8
+ declare const REASON: {
9
+ readonly scroll: "scroll";
10
+ readonly resize: "resize";
11
+ readonly lock: "lock";
12
+ readonly unlock: "unlock";
13
+ };
14
+ //#endregion
15
+ //#region src/model/types.d.ts
16
+ type StickyMode = (typeof MODE)[keyof typeof MODE];
17
+ type StickyReason = (typeof REASON)[keyof typeof REASON];
18
+ interface Box {
19
+ top: number;
20
+ bottom: number;
21
+ width: number;
22
+ height: number;
23
+ }
24
+ interface StickyModel {
25
+ mode: StickyMode;
26
+ containerTop: number;
27
+ locked: boolean;
28
+ lockedTop: number | null;
29
+ }
30
+ interface StickyFrame {
31
+ disabled: boolean;
32
+ locked: boolean;
33
+ allowLockedOverflowTop: boolean;
34
+ allowLockedOverflowBottom: boolean;
35
+ scrollingUp: boolean;
36
+ scrollY: number;
37
+ viewportHeight: number;
38
+ documentHeight: number;
39
+ wrapper: Box;
40
+ content: Box;
41
+ measure: Box;
42
+ reason: StickyReason;
43
+ }
44
+ //#endregion
45
+ //#region src/types.d.ts
46
+ interface ScrollMetrics {
47
+ scrollY: number;
48
+ viewportHeight: number;
49
+ documentHeight: number;
50
+ }
51
+ type StickyAsideState = 'flow' | 'top' | 'bottom' | 'locked' | 'off';
52
+ interface StickyAsidePresenterOptions {
53
+ disabled?: boolean;
54
+ locked?: boolean;
55
+ allowLockedOverflowTop?: boolean;
56
+ allowLockedOverflowBottom?: boolean;
57
+ onStateChange?: (state: StickyAsideState) => void;
58
+ }
59
+ interface StickyAsideClasses {
60
+ topClass?: string;
61
+ bottomClass?: string;
62
+ contentClass?: string;
63
+ }
64
+ interface StickyAsideOptions extends StickyAsidePresenterOptions {
65
+ wrapper: HTMLElement;
66
+ content: HTMLElement;
67
+ topClass?: string;
68
+ bottomClass?: string;
69
+ contentClass?: string;
70
+ getScrollMetrics?: () => ScrollMetrics;
71
+ readRect?: (element: HTMLElement) => Box;
72
+ }
73
+ interface StickyReading {
74
+ metrics: ScrollMetrics;
75
+ wrapper: Box;
76
+ content: Box;
77
+ measure: Box;
78
+ }
79
+ interface StickyPaintOff {
80
+ state: 'off';
81
+ }
82
+ interface StickyPaintFlow {
83
+ state: 'flow';
84
+ top: number;
85
+ }
86
+ interface StickyPaintTop {
87
+ state: 'top';
88
+ width: number;
89
+ }
90
+ interface StickyPaintBottom {
91
+ state: 'bottom';
92
+ width: number;
93
+ }
94
+ interface StickyPaintLocked {
95
+ state: 'locked';
96
+ top: number;
97
+ width: number;
98
+ }
99
+ type StickyPaint = StickyPaintOff | StickyPaintFlow | StickyPaintTop | StickyPaintBottom | StickyPaintLocked;
100
+ interface StickyAsideHandlers {
101
+ scroll(): void;
102
+ resize(): void;
103
+ }
104
+ interface StickyAsideView {
105
+ read(): StickyReading;
106
+ setClasses(classes: StickyAsideClasses): void;
107
+ paintMeasure(): void;
108
+ paint(paint: StickyPaint): void;
109
+ subscribe(handlers: StickyAsideHandlers): () => void;
110
+ destroy(): void;
111
+ }
112
+ interface StickyAsideController {
113
+ update: (reason?: Extract<StickyReason, 'scroll' | 'resize'>) => void;
114
+ setLocked: (isLocked: boolean) => void;
115
+ setDisabled: (isDisabled: boolean) => void;
116
+ setClasses: (classes: StickyAsideClasses) => void;
117
+ setAllowLockedOverflow: (isAllowTop: boolean, isAllowBottom: boolean) => void;
118
+ destroy: () => void;
119
+ getState: () => StickyAsideState;
120
+ getModel: () => StickyModel;
121
+ }
122
+ //#endregion
123
+ export { StickyFrame as _, StickyAsideOptions as a, StickyReason as b, StickyAsideView as c, StickyPaintFlow as d, StickyPaintLocked as f, Box as g, StickyReading as h, StickyAsideHandlers as i, StickyPaint as l, StickyPaintTop as m, StickyAsideClasses as n, StickyAsidePresenterOptions as o, StickyPaintOff as p, StickyAsideController as r, StickyAsideState as s, ScrollMetrics as t, StickyPaintBottom as u, StickyMode as v, StickyModel as y };
124
+ //# sourceMappingURL=types.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,44 @@
1
1
  {
2
2
  "name": "@sticky-aside/core",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "1.0.0",
4
+ "description": "A panel that follows page scroll and pins to the edge you are scrolling toward.",
5
+ "keywords": [
6
+ "sticky",
7
+ "sidebar",
8
+ "aside",
9
+ "scroll"
10
+ ],
11
+ "license": "MIT",
12
+ "author": "maprox",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/maprox/sticky-aside.git",
16
+ "directory": "packages/core"
17
+ },
18
+ "homepage": "https://github.com/maprox/sticky-aside#readme",
19
+ "bugs": "https://github.com/maprox/sticky-aside/issues",
20
+ "type": "module",
21
+ "sideEffects": [
22
+ "*.css"
23
+ ],
24
+ "imports": {
25
+ "#core/*": "./src/*"
26
+ },
27
+ "exports": {
28
+ ".": {
29
+ "types": "./dist/index.d.ts",
30
+ "import": "./dist/index.js"
31
+ },
32
+ "./headless": {
33
+ "types": "./dist/headless.d.ts",
34
+ "import": "./dist/headless.js"
35
+ },
36
+ "./offsets.css": "./dist/offsets.css"
37
+ },
38
+ "files": [
39
+ "dist"
40
+ ],
41
+ "scripts": {
42
+ "build": "tsdown --config tsdown.config.mjs"
43
+ }
6
44
  }