@linear_non/stellar-libs 1.0.37 → 1.0.39

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linear_non/stellar-libs",
3
- "version": "1.0.37",
3
+ "version": "1.0.39",
4
4
  "description": "Reusable JavaScript libraries for Non-Linear Studio projects.",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -25,7 +25,7 @@
25
25
  "author": "Non-Linear Studio",
26
26
  "license": "MIT",
27
27
  "dependencies": {
28
- "@linear_non/stellar-kit": "^2.1.9"
28
+ "@linear_non/stellar-kit": "^2.1.11"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@linear_non/prettier-config": "^1.0.6",
@@ -12,6 +12,9 @@ export default class Smooth {
12
12
  this.sections = null
13
13
  this.scrollbar = null
14
14
  this.dpr = Math.max(1, Math.round(window.devicePixelRatio || 1))
15
+
16
+ this._resizeRaf = null
17
+
15
18
  this.init()
16
19
  }
17
20
 
@@ -27,18 +30,16 @@ export default class Smooth {
27
30
 
28
31
  this.sections = []
29
32
  let cursor = 0
30
- let max = 0
31
33
 
32
34
  this.elems.forEach(el => {
33
35
  el.style.transform = "translate3d(0, 0, 0)"
36
+
34
37
  const speed = el.dataset.speed || 1
35
38
  const { height, offset } = this.getVars(el, speed)
36
39
 
37
40
  const top = cursor
38
41
  const bottom = top + height
39
-
40
42
  cursor = bottom
41
- max = bottom
42
43
 
43
44
  let parent = el.parentNode.closest("[data-smooth]")
44
45
 
@@ -119,35 +120,30 @@ export default class Smooth {
119
120
  }
120
121
 
121
122
  resize = () => {
122
- if (!this.sections) return
123
-
124
- let cursor = 0
125
- let max = 0
123
+ const { isSmooth } = kitStore.flags
124
+ if (!isSmooth) return
126
125
 
127
- this.sections.forEach(section => {
128
- section.el.style.transform = "translate3d(0, 0, 0)"
129
- const { height, offset } = this.getVars(section.el, section.speed)
126
+ if (this._resizeRaf) cancelAnimationFrame(this._resizeRaf)
130
127
 
131
- const top = cursor
132
- const bottom = top + height
128
+ this._resizeRaf = requestAnimationFrame(() => {
129
+ this._resizeRaf = null
133
130
 
134
- cursor = bottom
135
- max = bottom
131
+ this.update(this.elems)
136
132
 
137
- Object.assign(section, { top, bottom, offset })
133
+ emitter.emit(EVENTS.APP_SMOOTH_RESIZE)
138
134
  })
139
-
140
- emitter.emit(EVENTS.APP_SMOOTH_RESIZE)
141
- this.transformSections()
142
135
  }
143
136
 
144
137
  update(elems) {
145
138
  kitStore.flags.isResizing = true
139
+
146
140
  this.scroll.setScrollBounds()
147
141
  this.elems = elems || qsa("[data-smooth]")
148
142
  this.scrollbar?.update()
143
+
149
144
  this.getSections()
150
145
  this.transformSections()
146
+
151
147
  kitStore.flags.isResizing = false
152
148
  }
153
149
 
@@ -1,10 +1,19 @@
1
1
  import { EVENTS } from "@linear_non/stellar-kit/events"
2
2
  import { Observer, reverseSplit, splitText } from "@linear_non/stellar-kit/plugins"
3
3
 
4
+ const NOOP = () => {}
5
+
6
+ function normalizeTargets(splitTargets) {
7
+ if (!splitTargets) return []
8
+ if (splitTargets instanceof Element) return [splitTargets]
9
+ if (Array.isArray(splitTargets)) return splitTargets
10
+ return Array.from(splitTargets)
11
+ }
12
+
4
13
  export default class SplitonScroll {
5
14
  constructor({
6
15
  el,
7
- splitText,
16
+ splitText: splitTargets,
8
17
  isReady,
9
18
  reverse,
10
19
  start = "top bottom",
@@ -13,55 +22,58 @@ export default class SplitonScroll {
13
22
  once = true,
14
23
  }) {
15
24
  this.element = el
25
+ this.targets = normalizeTargets(splitTargets)
16
26
 
17
- // Normalize split targets to an array
18
- if (!splitText) {
19
- this.targets = []
20
- } else if (splitText instanceof Element) {
21
- this.targets = [splitText]
22
- } else if (Array.isArray(splitText)) {
23
- this.targets = splitText
24
- } else {
25
- this.targets = Array.from(splitText)
26
- }
27
-
28
- // SplitText() instance
29
27
  this.splits = null
30
-
31
- // Grouped SplitText() instances from data-split-group
32
28
  this.groups = null
33
29
 
34
- this.isReadyCallback = isReady || (() => {})
35
- this.reverseCallback = reverse || (() => {})
30
+ this.isReadyCallback = isReady || NOOP
31
+ this.reverseCallback = reverse || NOOP
36
32
  this.start = start
37
33
  this.end = end
38
34
  this.scrub = scrub
39
35
  this.once = once
40
36
 
41
- // Expose a Promise so users can wait for ready event
37
+ this._readyNotified = false
42
38
  this._resolveReady = null
43
39
  this.ready = new Promise(resolve => {
44
40
  this._resolveReady = resolve
45
41
  })
46
42
 
47
- // If no element or no targets, resolve immediately
43
+ this._onResize = this._onResize.bind(this)
44
+ this._resizeScheduled = false
45
+ this._offResize = null
46
+
47
+ // No element or no targets → resolve immediately once and bail
48
48
  if (!this.element || !this.targets.length) {
49
49
  const payload = { splits: [], groups: {} }
50
- if (this._resolveReady) {
51
- this._resolveReady(payload)
52
- this._resolveReady = null
53
- }
54
- this.isReadyCallback([], {})
50
+ this._notifyReadyOnce(payload)
55
51
  return
56
52
  }
57
53
 
58
54
  this.addObserver()
55
+ this.addResizeListener()
59
56
 
60
- if (this.observer.isActive || this.observer.progress > 0) {
57
+ // If already in view on init, run enter logic once
58
+ if (this.observer && (this.observer.isActive || this.observer.progress > 0)) {
61
59
  this.handleEnter()
62
60
  }
63
61
  }
64
62
 
63
+ _notifyReadyOnce(payload) {
64
+ if (this._readyNotified) return
65
+ this._readyNotified = true
66
+
67
+ // external callback
68
+ this.isReadyCallback(payload.splits, payload.groups)
69
+
70
+ // external promise
71
+ if (this._resolveReady) {
72
+ this._resolveReady(payload)
73
+ this._resolveReady = null
74
+ }
75
+ }
76
+
65
77
  addObserver() {
66
78
  this.observer = new Observer({
67
79
  trigger: this.element,
@@ -75,29 +87,64 @@ export default class SplitonScroll {
75
87
  this.observer.on("leave", () => this.handleLeave())
76
88
  }
77
89
 
90
+ addResizeListener() {
91
+ this._offResize = emitter.on(EVENTS.APP_RESIZE, this._onResize, PRIORITY.first)
92
+ }
93
+
94
+ removeResizeListener() {
95
+ if (this._offResize) {
96
+ this._offResize()
97
+ this._offResize = null
98
+ }
99
+ }
100
+
101
+ _onResize() {
102
+ this._handleResizeInternal()
103
+ }
104
+
105
+ _handleResizeInternal() {
106
+ if (!this.element || !this.targets || !this.targets.length) return
107
+
108
+ if (this.splits) {
109
+ reverseSplit(this.splits)
110
+ }
111
+
112
+ if (this.observer && typeof this.observer.refresh === "function") {
113
+ this.observer.refresh()
114
+ }
115
+
116
+ // If in view after resize, re-split; external callback will *not* re-fire
117
+ if (this.observer && (this.observer.isActive || this.observer.progress > 0)) {
118
+ this.handleEnter()
119
+ }
120
+ }
121
+
78
122
  async handleEnter() {
79
- const emitter = splitText(this.targets)
123
+ const splitEmitter = splitText(this.targets)
80
124
 
81
125
  const { splits, groups } = await new Promise(resolve => {
82
- emitter.on(EVENTS.APP_SPLITTEXT_READY, (readySplits, groupMap) => {
126
+ const onReady = (readySplits, groupMap) => {
127
+ if (typeof splitEmitter.off === "function") {
128
+ splitEmitter.off(EVENTS.APP_SPLITTEXT_READY, onReady)
129
+ }
83
130
  resolve({
84
131
  splits: readySplits || [],
85
132
  groups: groupMap || {},
86
133
  })
87
- })
134
+ }
135
+
136
+ if (typeof splitEmitter.once === "function") {
137
+ splitEmitter.once(EVENTS.APP_SPLITTEXT_READY, onReady)
138
+ } else {
139
+ splitEmitter.on(EVENTS.APP_SPLITTEXT_READY, onReady)
140
+ }
88
141
  })
89
142
 
90
143
  this.splits = splits
91
144
  this.groups = groups
92
145
 
93
- const payload = { splits, groups }
94
-
95
- if (this._resolveReady) {
96
- this._resolveReady(payload)
97
- this._resolveReady = null
98
- }
99
-
100
- this.isReadyCallback(splits, groups)
146
+ // First time notify + resolve; later (resize, re-enter) → no-op
147
+ this._notifyReadyOnce({ splits, groups })
101
148
  }
102
149
 
103
150
  handleLeave() {
@@ -118,6 +165,12 @@ export default class SplitonScroll {
118
165
 
119
166
  destroy() {
120
167
  this.observer?.kill()
168
+ this.removeResizeListener()
169
+
170
+ if (this.splits) {
171
+ reverseSplit(this.splits)
172
+ }
173
+
121
174
  this.splits = null
122
175
  this.groups = null
123
176
  this.targets = null