@linear_non/stellar-kit 2.1.21 → 3.0.1

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.
@@ -1,26 +1,63 @@
1
- // Manager.js
2
1
  import { qsa } from "../utils"
3
2
 
3
+ /**
4
+ * @typedef {import("./Component").default} BaseComponent
5
+ */
6
+
7
+ /**
8
+ * Component life cycle manager.
9
+ *
10
+ * @typedef {Object} ComponentDefinition
11
+ * @property {string} name
12
+ * @property {new (options: any) => BaseComponent} instance
13
+ * @property {any} [data]
14
+ */
4
15
  export default class Manager {
16
+ /**
17
+ * @param {{ data?: any }} [options]
18
+ */
5
19
  constructor({ data = {} } = {}) {
20
+ /** @type {{ name: string; component: BaseComponent }[]} */
6
21
  this.components = [] // { name, component }
22
+
23
+ /** @type {any} */
7
24
  this.data = data
8
25
  }
9
26
 
27
+ /**
28
+ * Get all components by name.
29
+ *
30
+ * @param {string} name
31
+ * @returns {BaseComponent[]}
32
+ */
10
33
  getComponents(name) {
11
34
  return this.components.filter(c => c.name === name).map(c => c.component)
12
35
  }
13
36
 
37
+ /**
38
+ * Get a single component by name and index.
39
+ *
40
+ * @param {string} name
41
+ * @param {number} [index=0]
42
+ * @returns {BaseComponent|null}
43
+ */
14
44
  getComponent(name, index = 0) {
15
45
  return this.getComponents(name)[index] || null
16
46
  }
17
47
 
48
+ /**
49
+ * Add components based on definition.
50
+ *
51
+ * @param {ComponentDefinition} def
52
+ * @param {any} [renderer=null]
53
+ * @returns {void}
54
+ */
18
55
  addComponent(def, renderer = null) {
19
56
  if (!def || !def.instance || !def.name) return
20
57
 
21
58
  const { name, instance, data = null } = def
22
59
 
23
- // resolve selector from name
60
+ /** @type {string} */
24
61
  let selector
25
62
  if (name.startsWith(".") || name.startsWith("[")) {
26
63
  selector = name
@@ -49,7 +86,14 @@ export default class Manager {
49
86
  })
50
87
  }
51
88
 
52
- // INTERNAL CALLER
89
+ /**
90
+ * Call a method on all components.
91
+ *
92
+ * @param {keyof BaseComponent} method
93
+ * @param {...any} args
94
+ * @returns {void}
95
+ * @private
96
+ */
53
97
  _call(method, ...args) {
54
98
  this.components.forEach(entry => {
55
99
  const c = entry.component
@@ -70,6 +114,9 @@ export default class Manager {
70
114
  this._call("animateOut")
71
115
  }
72
116
 
117
+ /**
118
+ * @param {any} obj
119
+ */
73
120
  tick(obj) {
74
121
  this._call("tick", obj)
75
122
  }
@@ -0,0 +1,299 @@
1
+ // Application.js
2
+ import { kitStore } from "../kitStore.js"
3
+
4
+ // Events
5
+ import { emitter, EVENTS, Scroll } from "../events/index.js"
6
+ import Resize from "../events/Resize.js"
7
+ import Mouse from "../events/Mouse.js"
8
+ import Raf from "../events/Raf.js"
9
+
10
+ // Libraries
11
+ import { ScrollTrigger } from "../libraries/gsap/index.js"
12
+
13
+ // Utils
14
+ import { sniffer } from "../utils/sniffer.js"
15
+ import { qs } from "../utils/selector.js"
16
+ import { Debug } from "../utils/debug.js"
17
+
18
+ /**
19
+ * Core lifecycle manager for all global systems:
20
+ * - Resize
21
+ * - Mouse
22
+ * - Scroll
23
+ * - Raf (ticker)
24
+ *
25
+ * Responsible for:
26
+ * - Store initialization
27
+ * - System initialization and activation
28
+ * - Router integration
29
+ * - View transitions
30
+ * - DOM readiness handling
31
+ */
32
+ export class ApplicationManager {
33
+ constructor() {
34
+ /**
35
+ * Whether the application completed initialization.
36
+ * @type {boolean}
37
+ */
38
+ this.initialized = false
39
+
40
+ /**
41
+ * Debug mode state.
42
+ * @type {boolean}
43
+ */
44
+ this.isDebug = false
45
+
46
+ /**
47
+ * Global system instances (initialized in _initSystems).
48
+ * @type {{
49
+ * resize: Resize|null,
50
+ * mouse: Mouse|null,
51
+ * scroll: Scroll|null,
52
+ * raf: Raf|null
53
+ * }}
54
+ */
55
+ this.systems = {
56
+ resize: null,
57
+ mouse: null,
58
+ scroll: null,
59
+ raf: null,
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Enable or disable debug logging.
65
+ * @param {boolean} [state=true]
66
+ * @returns {ApplicationManager}
67
+ */
68
+ debug(state = true) {
69
+ Debug[state ? "enable" : "disable"]()
70
+ return this
71
+ }
72
+
73
+ /**
74
+ * Initialize internal state store and environment flags.
75
+ * @param {{
76
+ * url: URL|string,
77
+ * page?: { element?: string, container?: string },
78
+ * load?: string,
79
+ * dim?: { d?: number, m?: number }
80
+ * }} config
81
+ * @private
82
+ */
83
+ _initStore({ url, page, load, dim }) {
84
+ const { element, container } = page || {}
85
+ const isHomePage = url.pathname === "/" || url === "/"
86
+
87
+ kitStore.pageContent = container ? qs(container) : null
88
+ kitStore.currentPage = element ? qs(element) : null
89
+
90
+ kitStore.load = load ? qs(load) : null
91
+ kitStore.currentURL = url ?? null
92
+ kitStore.flags.isHomePage = Boolean(isHomePage)
93
+
94
+ if (dim) {
95
+ kitStore.sizes.d = dim.d ?? kitStore.sizes.d
96
+ kitStore.sizes.m = dim.m ?? kitStore.sizes.m
97
+ }
98
+
99
+ this._setSizes(kitStore.sizes.d, kitStore.sizes.m)
100
+
101
+ kitStore.flags.isLocked = true
102
+
103
+ Debug.log("APP", "Store initialized", {
104
+ url: kitStore.currentURL,
105
+ pageContent: kitStore.pageContent,
106
+ currentPage: kitStore.currentPage,
107
+ sizes: kitStore.sizes,
108
+ })
109
+ }
110
+
111
+ /**
112
+ * Initialize and activate all global systems.
113
+ * @param {{ isSmooth: boolean }} options
114
+ * @private
115
+ */
116
+ _initSystems({ isSmooth }) {
117
+ const resize = new Resize()
118
+ const mouse = new Mouse()
119
+ const scroll = new Scroll({ isSmooth: isSmooth })
120
+ const raf = new Raf()
121
+
122
+ this.systems = { resize, mouse, scroll, raf }
123
+
124
+ kitStore.resize = resize
125
+ kitStore.mouse = mouse
126
+ kitStore.scroll = scroll
127
+ kitStore.raf = raf
128
+
129
+ kitStore.flags.isSmooth = isSmooth
130
+
131
+ // Phase 1: init
132
+ Object.values(this.systems).forEach(sys => {
133
+ if (sys && typeof sys.init === "function") sys.init()
134
+ })
135
+
136
+ // Phase 2: activate listeners/tickers
137
+ this.on()
138
+
139
+ Debug.log("APP", "Systems initialized", this.systems)
140
+ }
141
+
142
+ /**
143
+ * Activate all system listeners (Resize, Mouse, Scroll, Raf).
144
+ */
145
+ on() {
146
+ Object.values(this.systems).forEach(sys => {
147
+ if (sys && typeof sys.on === "function") sys.on()
148
+ })
149
+ }
150
+
151
+ /**
152
+ * Deactivate all system listeners.
153
+ */
154
+ stop() {
155
+ Object.values(this.systems).forEach(sys => {
156
+ if (sys && typeof sys.off === "function") sys.off()
157
+ })
158
+ }
159
+
160
+ /**
161
+ * Fully destroy all systems and reset the application state.
162
+ */
163
+ destroy() {
164
+ this.stop()
165
+ Object.values(this.systems).forEach(sys => {
166
+ if (sys && typeof sys.destroy === "function") sys.destroy()
167
+ })
168
+ this.initialized = false
169
+ }
170
+
171
+ /**
172
+ * Initialize the full application lifecycle.
173
+ * Ensures DOMReady + store + systems + flags.
174
+ *
175
+ * @param {{
176
+ * url: URL|string,
177
+ * page: { element?: string, container?: string },
178
+ * load?: string,
179
+ * isSmooth?: boolean,
180
+ * dim?: { d?: number, m?: number }
181
+ * }} config
182
+ * @returns {Promise<typeof kitStore>}
183
+ */
184
+ async initialize(config) {
185
+ if (this.initialized) return kitStore
186
+ if (!sniffer.isDesktop) {
187
+ config.isSmooth = false
188
+ } else if (!config.isSmooth && sniffer.isDesktop) {
189
+ config.isSmooth = true
190
+ }
191
+
192
+ await this._waitDOM()
193
+ Debug.log("APP", "DOM ready with", config.isSmooth ? "smooth " : "native " + "scroll.")
194
+
195
+ this._initStore(config)
196
+ this._initSystems({ isSmooth: config.isSmooth })
197
+
198
+ this.initialized = true
199
+ Debug.log("APP", "Application ready.", kitStore)
200
+
201
+ return kitStore
202
+ }
203
+
204
+ /**
205
+ * Attach Taxi router events into the global lifecycle.
206
+ * @param {import("@unseenco/taxi").Core|null} router
207
+ */
208
+ attachTaxi(router) {
209
+ if (!router) return
210
+ kitStore.taxi = router
211
+
212
+ router.on("NAVIGATE_IN", () => {
213
+ const { currentLocation } = router
214
+ kitStore.currentURL = currentLocation
215
+
216
+ if (kitStore.raf?.setScroll) kitStore.raf.setScroll(0)
217
+
218
+ Debug.log("ROUTE", "NAVIGATE_IN")
219
+ emitter.emit(EVENTS.APP_ROUTE_IN, { url: currentLocation })
220
+ })
221
+
222
+ router.on("NAVIGATE_END", () => {
223
+ this._updatePageRefs()
224
+ this._afterViewChange()
225
+
226
+ Debug.log("ROUTE", "NAVIGATE_END")
227
+ emitter.emit(EVENTS.APP_ROUTE_END, { url: kitStore.currentURL })
228
+ })
229
+
230
+ router.on("NAVIGATE_OUT", () => {
231
+ Debug.log("ROUTE", "NAVIGATE_OUT")
232
+ emitter.emit(EVENTS.APP_ROUTE_OUT, { url: kitStore.currentURL })
233
+ })
234
+ }
235
+
236
+ /**
237
+ * Update references to current page and container after a Taxi view swap.
238
+ * @private
239
+ */
240
+ _updatePageRefs() {
241
+ const currentPage = qs("[data-taxi-view]")
242
+ const pageContent = kitStore.pageContent ? kitStore.pageContent : qs("#app")
243
+
244
+ kitStore.currentPage = currentPage || null
245
+ kitStore.pageContent = pageContent || null
246
+ }
247
+
248
+ /**
249
+ * Run post-navigation updates:
250
+ * - Reset Scroll bounds
251
+ * - Reset GSAP scroll proxy
252
+ * - Soft resize
253
+ * @private
254
+ */
255
+ _afterViewChange() {
256
+ if (this.systems.scroll?.setScrollBounds) {
257
+ this.systems.scroll.setScrollBounds()
258
+ }
259
+
260
+ if (this.systems.raf?.setScrollTrigger) {
261
+ this.systems.raf.setScrollTrigger()
262
+ }
263
+
264
+ this._softResize()
265
+ }
266
+
267
+ /**
268
+ * Set CSS variables for responsive layout.
269
+ * @param {number} d
270
+ * @param {number} m
271
+ * @private
272
+ */
273
+ _setSizes(d, m) {
274
+ document.documentElement.style.setProperty("--desktop", d)
275
+ document.documentElement.style.setProperty("--mobile", m)
276
+ }
277
+
278
+ /**
279
+ * Trigger a "soft resize":
280
+ * - Emit resize event
281
+ * - Refresh ScrollTrigger
282
+ * @private
283
+ */
284
+ _softResize() {
285
+ Debug.log("APP", "Soft resize triggered.")
286
+ emitter.emit(EVENTS.APP_SMOOTH_RESIZE)
287
+ ScrollTrigger.refresh()
288
+ }
289
+
290
+ /**
291
+ * Wait for DOM ready before execution.
292
+ * @returns {Promise<void>}
293
+ * @private
294
+ */
295
+ _waitDOM() {
296
+ if (document.readyState !== "loading") return Promise.resolve()
297
+ return new Promise(res => document.addEventListener("DOMContentLoaded", res, { once: true }))
298
+ }
299
+ }
@@ -0,0 +1,221 @@
1
+ import { Manager } from "../classes/index.js"
2
+ import { emitter, EVENTS } from "../events/index.js"
3
+ import { Debug, qsa } from "../utils/index.js"
4
+ import { kitStore } from "../kitStore.js"
5
+
6
+ /**
7
+ * @typedef {Object} EngineOwner
8
+ * The page instance passed from project-level Page (extends Renderer).
9
+ */
10
+
11
+ /**
12
+ * @typedef {Object} EngineScrollState
13
+ * @property {number} current - Current scroll value coming from Raf.
14
+ * @property {"up"|"down"} direction - Derived scroll direction.
15
+ */
16
+
17
+ /**
18
+ * @typedef {Object} EngineMouseState
19
+ * @property {number} x - Current mouse X.
20
+ * @property {number} y - Current mouse Y.
21
+ */
22
+
23
+ /**
24
+ * @typedef {Object} EngineSetupConfig
25
+ * @property {Array<any>} components - Array of component constructors/definitions.
26
+ * @property {any} smooth - Smooth instance for scroll control.
27
+ */
28
+
29
+ export default class PageEngine {
30
+ /**
31
+ * @param {{ owner: EngineOwner }} param0
32
+ */
33
+ constructor({ owner }) {
34
+ /**
35
+ * @type {EngineOwner}
36
+ */
37
+ this.owner = owner
38
+
39
+ /**
40
+ * @type {Array<any>}
41
+ */
42
+ this.components = []
43
+
44
+ /**
45
+ * @type {number}
46
+ */
47
+ this.componentsLength = 0
48
+
49
+ /**
50
+ * @type {Manager | null}
51
+ */
52
+ this.manager = null
53
+
54
+ /**
55
+ * Smooth instance passed in by project Page
56
+ * @type {any | null}
57
+ */
58
+ this.smooth = null
59
+
60
+ /**
61
+ * @type {EngineScrollState}
62
+ */
63
+ this.scroll = { current: 0, direction: "down" }
64
+
65
+ /**
66
+ * @type {EngineMouseState}
67
+ */
68
+ this.mouse = { x: 0, y: 0 }
69
+ }
70
+
71
+ /**
72
+ * Store component definitions.
73
+ * Page calls this during setComponents().
74
+ *
75
+ * @param {Array<any>} components
76
+ */
77
+ setComponents(components) {
78
+ Debug.log("ENGINE", "Set Components", { components })
79
+
80
+ this.components = components || []
81
+ }
82
+
83
+ /**
84
+ * Called by Page.initCore().
85
+ * Recreates Manager and binds a new Smooth instance.
86
+ *
87
+ * @param {EngineSetupConfig} param0
88
+ */
89
+ setup({ components, smooth }) {
90
+ Debug.log("ENGINE", "Setup", { components, smooth })
91
+
92
+ this.components = components || []
93
+ this.smooth = smooth || null
94
+
95
+ this.manager = new Manager()
96
+
97
+ if (this.manager && this.components.length) {
98
+ this.components.forEach(obj => {
99
+ this.manager.addComponent(obj, this.owner)
100
+ })
101
+ }
102
+
103
+ this.manager.initialize()
104
+ this.manager.animateIn?.()
105
+
106
+ this.on()
107
+ this.trackImages()
108
+ this.resize()
109
+ }
110
+
111
+ /**
112
+ * Adds `.loaded` class to images when loaded.
113
+ * Looks for `.lazy` images.
114
+ */
115
+ trackImages() {
116
+ const imgs = qsa(".lazy")
117
+ if (!imgs || !imgs.length) return
118
+
119
+ imgs.forEach(img => {
120
+ if (img.complete) {
121
+ img.classList.add("loaded")
122
+ return
123
+ }
124
+
125
+ img.addEventListener(
126
+ "load",
127
+ () => {
128
+ img.classList.add("loaded")
129
+ },
130
+ { once: true }
131
+ )
132
+ })
133
+ }
134
+
135
+ /**
136
+ * Receives scroll and mouse data from Raf (APP_TICK).
137
+ * Packages them and forwards to Manager.tick().
138
+ *
139
+ * @param {{ current: number, diff: number, mouse: EngineMouseState }} param0
140
+ */
141
+ tick = ({ current, diff, mouse }) => {
142
+ const { isResizing, isLocked } = kitStore.flags
143
+
144
+ if (isResizing || isLocked) return
145
+ if (!this.manager) return
146
+
147
+ this.scroll.current = current
148
+ this.scroll.direction = diff > 0 ? "down" : "up"
149
+
150
+ this.mouse.x = mouse.x
151
+ this.mouse.y = mouse.y
152
+
153
+ this.manager.tick?.({
154
+ mouse: this.mouse,
155
+ scroll: this.scroll,
156
+ })
157
+ }
158
+
159
+ /**
160
+ * Called during APP_RESIZE.
161
+ * Updates Smooth then calls Manager.resize().
162
+ */
163
+ resize = () => {
164
+ Debug.log("ENGINE", "Resize")
165
+
166
+ this.smooth?.update?.()
167
+ this.manager?.resize?.()
168
+ }
169
+
170
+ /**
171
+ * Called during APP_SMOOTH_RESIZE.
172
+ * Delegates to Manager.smoothResize().
173
+ */
174
+ smoothResize = () => {
175
+ Debug.log("ENGINE", "Smooth Resize")
176
+
177
+ this.manager?.smoothResize?.()
178
+ }
179
+
180
+ /**
181
+ * Internal listener binder/unbinder.
182
+ *
183
+ * @param {"on" | "off"} action
184
+ */
185
+ listeners(action) {
186
+ Debug.log("ENGINE", `Listeners`, action.toUpperCase())
187
+
188
+ emitter[action](EVENTS.APP_TICK, this.tick)
189
+ emitter[action](EVENTS.APP_RESIZE, this.resize)
190
+ emitter[action](EVENTS.APP_SMOOTH_RESIZE, this.smoothResize)
191
+ }
192
+
193
+ /**
194
+ * Register RAF listeners.
195
+ */
196
+ on() {
197
+ this.listeners("on")
198
+ }
199
+
200
+ /**
201
+ * Unregister RAF listeners.
202
+ */
203
+ off() {
204
+ this.listeners("off")
205
+ this.destroy()
206
+ }
207
+
208
+ /**
209
+ * Cleanup Smooth + Manager.
210
+ * Called by Page.onLeaveCompleted().
211
+ */
212
+ destroy() {
213
+ this.manager?.destroy?.()
214
+ this.manager = null
215
+
216
+ if (this.smooth) {
217
+ this.smooth.destroy?.()
218
+ this.smooth = null
219
+ }
220
+ }
221
+ }
package/core/index.js ADDED
@@ -0,0 +1,8 @@
1
+ import { ApplicationManager } from "./Application"
2
+ import PageEngine from "./PageEngine"
3
+
4
+ /**
5
+ * @type {ApplicationManager}
6
+ */
7
+ export const Application = new ApplicationManager()
8
+ export { PageEngine }
package/events/Emitter.js CHANGED
@@ -6,6 +6,7 @@ class Emitter {
6
6
 
7
7
  emit(event, ...args) {
8
8
  const callbacks = this.events[event] || []
9
+
9
10
  for (let i = 0, { length } = callbacks; i < length; i++) {
10
11
  callbacks[i].cb(...args)
11
12
  }
@@ -15,6 +16,7 @@ class Emitter {
15
16
  const data = { cb, priority }
16
17
  this.events[event]?.push(data) || (this.events[event] = [data])
17
18
  this.events[event].sort((a, b) => a.priority - b.priority)
19
+
18
20
  return () => {
19
21
  this.events[event] = this.events[event]?.filter(v => cb !== v.cb)
20
22
  }
@@ -29,9 +31,7 @@ class Emitter {
29
31
  cb(...args)
30
32
  this.off(event, onceCallback)
31
33
  }
32
-
33
34
  this.on(event, onceCallback, priority)
34
-
35
35
  return () => {
36
36
  this.off(event, cb)
37
37
  }
@@ -45,6 +45,7 @@ class Emitter {
45
45
  const emitter = new Emitter()
46
46
 
47
47
  const EVENTS = {
48
+ APP_UNLOCKED: "app:unlocked",
48
49
  APP_TICK: "tick",
49
50
  APP_RESIZE: "resize",
50
51
  APP_SMOOTH_RESIZE: "smooth:resize",
package/events/Raf.js CHANGED
@@ -1,5 +1,4 @@
1
- // Raf.js
2
- import kitStore from "../kitStore"
1
+ import { kitStore } from "../kitStore"
3
2
  import emitter, { EVENTS } from "./Emitter"
4
3
  import { lerp } from "../utils"
5
4
  import { gsap, ScrollTrigger } from "../libraries/gsap"
@@ -19,14 +18,50 @@ export default class Raf {
19
18
  target: null,
20
19
  }
21
20
 
21
+ this.diff = 0
22
+
23
+ this.tick = this.tick.bind(this)
24
+ this.onScroll = this.onScroll.bind(this)
25
+ this.onMouseMove = this.onMouseMove.bind(this)
26
+ this.onAppResize = this.onAppResize.bind(this)
27
+
28
+ this.onPageHide = this.stop.bind(this)
29
+ this.onPageShow = this.resume.bind(this)
30
+ }
31
+
32
+ init() {
22
33
  this.setScrollTrigger()
23
- this.on()
24
34
  }
25
35
 
26
- onAppResize = () => {
36
+ on() {
37
+ gsap.ticker.add(this.tick)
38
+
39
+ emitter.on(EVENTS.APP_SCROLL, this.onScroll)
40
+ emitter.on(EVENTS.APP_MOUSEMOVE, this.onMouseMove)
41
+ emitter.on(EVENTS.APP_SMOOTH_RESIZE, this.onAppResize)
42
+
43
+ window.addEventListener("pagehide", this.onPageHide)
44
+ window.addEventListener("pageshow", this.onPageShow)
45
+ }
46
+
47
+ off() {
48
+ gsap.ticker.remove(this.tick)
49
+
50
+ emitter.off(EVENTS.APP_SCROLL, this.onScroll)
51
+ emitter.off(EVENTS.APP_MOUSEMOVE, this.onMouseMove)
52
+ emitter.off(EVENTS.APP_SMOOTH_RESIZE, this.onAppResize)
53
+
54
+ window.removeEventListener("pagehide", this.onPageHide)
55
+ window.removeEventListener("pageshow", this.onPageShow)
56
+ }
57
+
58
+ destroy() {
59
+ this.off()
60
+ }
61
+
62
+ onAppResize() {
27
63
  const { fh } = kitStore.sizes
28
64
 
29
- // clamp our smooth state
30
65
  this.scroll.target = Math.min(Math.max(this.scroll.target, 0), fh)
31
66
  this.scroll.current = Math.min(Math.max(this.scroll.current, 0), fh)
32
67
  this.scroll.rounded = Math.min(Math.max(this.scroll.rounded, 0), fh)
@@ -34,8 +69,9 @@ export default class Raf {
34
69
  ScrollTrigger.update()
35
70
  }
36
71
 
37
- tick = () => {
72
+ tick() {
38
73
  if (kitStore.flags.isResizing) return
74
+
39
75
  const { target, current, ease } = this.scroll
40
76
 
41
77
  this.scroll.current = lerp(current, target, ease)
@@ -55,7 +91,6 @@ export default class Raf {
55
91
  getScroll() {
56
92
  const { pageContent, flags } = kitStore
57
93
  const container = pageContent ? pageContent.parentNode : document.body
58
-
59
94
  return flags.isSmooth ? this.scroll.rounded : container.scrollTop
60
95
  }
61
96
 
@@ -64,13 +99,13 @@ export default class Raf {
64
99
  this.scroll.target = Math.min(Math.max(this.scroll.target, 0), fh)
65
100
  }
66
101
 
67
- onScroll = ({ y }) => {
102
+ onScroll({ y }) {
68
103
  if (kitStore.flags.isLocked) return
69
104
  this.scroll.target += y
70
105
  this.clamp()
71
106
  }
72
107
 
73
- onMouseMove = ({ x, y, target }) => {
108
+ onMouseMove({ x, y, target }) {
74
109
  this.mouse.x = x
75
110
  this.mouse.y = y
76
111
  this.mouse.target = target
@@ -102,52 +137,30 @@ export default class Raf {
102
137
 
103
138
  setScrollTrigger() {
104
139
  const { pageContent } = kitStore
140
+ const scroller = pageContent || document.body
105
141
 
106
142
  ScrollTrigger.defaults({
107
- scroller: pageContent,
143
+ scroller,
108
144
  })
109
145
 
110
- ScrollTrigger.scrollerProxy(pageContent, {
146
+ ScrollTrigger.scrollerProxy(scroller, {
111
147
  scrollTop: () => {
112
148
  return this.getScroll()
113
149
  },
114
150
  getBoundingClientRect() {
115
- // Important that width and height are dynamic
116
151
  return { top: 0, left: 0, width: window.innerWidth, height: window.innerHeight }
117
152
  },
118
153
  })
119
154
  }
120
155
 
121
- stop = () => {
156
+ stop() {
122
157
  gsap.ticker.remove(this.tick)
123
158
  }
124
159
 
125
- resume = () => {
160
+ resume() {
126
161
  this.scroll.current = this.scroll.target
127
162
  this.scroll.rounded = this.scroll.target
128
163
  gsap.ticker.add(this.tick)
129
164
  ScrollTrigger.update()
130
165
  }
131
-
132
- on() {
133
- gsap.ticker.add(this.tick)
134
- emitter.on(EVENTS.APP_SCROLL, this.onScroll)
135
- emitter.on(EVENTS.APP_MOUSEMOVE, this.onMouseMove)
136
- emitter.on(EVENTS.APP_SMOOTH_RESIZE, this.onAppResize)
137
- window.addEventListener("pagehide", () => this.stop)
138
- window.addEventListener("pageshow", () => this.resume)
139
- }
140
-
141
- off() {
142
- gsap.ticker.remove(this.tick)
143
- emitter.off(EVENTS.APP_SCROLL, this.onScroll)
144
- emitter.off(EVENTS.APP_MOUSEMOVE, this.onMouseMove)
145
- emitter.off(EVENTS.APP_SMOOTH_RESIZE, this.onAppResize)
146
- window.removeEventListener("pagehide", () => this.stop)
147
- window.removeEventListener("pageshow", () => this.resume)
148
- }
149
-
150
- destroy() {
151
- this.off()
152
- }
153
166
  }
package/events/Resize.js CHANGED
@@ -1,4 +1,3 @@
1
- // Resize.js
2
1
  import { sizes, flags, breakpoints } from "../kitStore"
3
2
  import emitter, { EVENTS } from "./Emitter"
4
3
  import debounce from "lodash.debounce"
@@ -8,7 +7,27 @@ import { ScrollTrigger } from "../libraries/gsap"
8
7
  export default class Resize {
9
8
  constructor() {
10
9
  this.wasDesktop = sniffer.isDesktop
11
- this.init()
10
+
11
+ this.handleResize = debounce(this.onResize.bind(this), 200)
12
+ this.handleLoadOnce = this.onLoadOnce.bind(this)
13
+ }
14
+
15
+ init() {
16
+ this.onResize()
17
+ }
18
+
19
+ on() {
20
+ window.addEventListener("resize", this.handleResize, { passive: true })
21
+ window.addEventListener("load", this.handleLoadOnce, { once: true })
22
+ }
23
+
24
+ off() {
25
+ window.removeEventListener("resize", this.handleResize)
26
+ window.removeEventListener("load", this.handleLoadOnce)
27
+ }
28
+
29
+ destroy() {
30
+ this.off()
12
31
  }
13
32
 
14
33
  onResize = () => {
@@ -16,7 +35,7 @@ export default class Resize {
16
35
 
17
36
  const { width, height } = getViewport()
18
37
  const bp = getWindowSizes()
19
- sniffer.update() // Update sniffer after viewport read
38
+ sniffer.update()
20
39
 
21
40
  Object.assign(sizes, {
22
41
  vw: width,
@@ -69,14 +88,4 @@ export default class Resize {
69
88
  document.body.classList.toggle("is-desktop", isDesktop)
70
89
  document.body.classList.toggle("is-device", !isDesktop)
71
90
  }
72
-
73
- on() {
74
- window.addEventListener("resize", debounce(this.onResize, 200), { passive: true })
75
- window.addEventListener("load", this.onLoadOnce, { once: true })
76
- }
77
-
78
- init() {
79
- this.on()
80
- this.onResize() // Run once on init
81
- }
82
91
  }
package/events/Scroll.js CHANGED
@@ -1,4 +1,3 @@
1
- // Scroll.js
2
1
  import { sizes } from "../kitStore"
3
2
  import emitter, { EVENTS } from "./Emitter"
4
3
  import VirtualScroll from "./VirtualScroll"
@@ -7,24 +6,52 @@ import { bounds, sniffer } from "../utils"
7
6
  export default class Scroll {
8
7
  constructor({ isSmooth = false } = {}) {
9
8
  this.isSmooth = isSmooth
9
+ this.virtualScroll = null
10
10
 
11
+ this.onVirtualScroll = this.onVirtualScroll.bind(this)
12
+ this.onScroll = this.onScroll.bind(this)
13
+ }
14
+
15
+ init() {
16
+ this.setScrollBounds()
17
+ }
18
+
19
+ on() {
11
20
  if (this.isSmooth) {
12
21
  document.body.classList.add("is-smooth")
13
22
 
14
- this.virtualScroll = new VirtualScroll({
15
- mouseMultiplier: sniffer.isWindows ? 1.1 : 0.45,
16
- touchMultiplier: 3.5,
17
- firefoxMultiplier: sniffer.isWindows ? 40 : 90,
23
+ if (!this.virtualScroll) {
24
+ this.virtualScroll = new VirtualScroll({
25
+ mouseMultiplier: sniffer.isWindows ? 1.1 : 0.45,
26
+ touchMultiplier: 3.5,
27
+ firefoxMultiplier: sniffer.isWindows ? 40 : 90,
28
+ passive: true,
29
+ })
30
+ }
31
+
32
+ this.virtualScroll.on(this.onVirtualScroll)
33
+ } else {
34
+ document.addEventListener("scroll", this.onScroll, {
18
35
  passive: true,
36
+ capture: true,
19
37
  })
38
+ }
39
+ }
20
40
 
21
- this.virtualScroll.on(this.onVirtualScroll)
41
+ off() {
42
+ if (this.virtualScroll) {
43
+ this.virtualScroll.off(this.onVirtualScroll)
22
44
  } else {
23
- this.onScroll = this.onScroll.bind(this)
24
- document.addEventListener("scroll", this.onScroll, true)
45
+ document.removeEventListener("scroll", this.onScroll, true)
25
46
  }
47
+ }
26
48
 
27
- this.setScrollBounds()
49
+ destroy() {
50
+ this.off()
51
+ if (this.virtualScroll) {
52
+ this.virtualScroll.destroy()
53
+ this.virtualScroll = null
54
+ }
28
55
  }
29
56
 
30
57
  setScrollBounds() {
@@ -33,19 +60,11 @@ export default class Scroll {
33
60
  sizes.fh = height > sizes.vh ? height - sizes.vh : 0
34
61
  }
35
62
 
36
- onVirtualScroll = e => {
63
+ onVirtualScroll(e) {
37
64
  emitter.emit(EVENTS.APP_SCROLL, { y: e.deltaY * -1 })
38
65
  }
39
66
 
40
- onScroll(e) {
67
+ onScroll() {
41
68
  emitter.emit(EVENTS.APP_SCROLL, { y: window.scrollY })
42
69
  }
43
-
44
- destroy() {
45
- if (this.virtualScroll) {
46
- this.virtualScroll.destroy()
47
- } else {
48
- document.removeEventListener("scroll", this.onScroll, true)
49
- }
50
- }
51
70
  }
@@ -1,5 +1,5 @@
1
1
  // VirtualScroll.js
2
- import kitStore from "../kitStore"
2
+ import { kitStore } from "../kitStore"
3
3
  import emitter from "./Emitter"
4
4
  import { supportMouseTouch } from "../utils"
5
5
  import Lethargy from "lethargy"
package/index.js CHANGED
@@ -1,4 +1,8 @@
1
- // stellar-kit/index.js
1
+ /**
2
+ * Legacy setup functions for Stellar Kit.
3
+ * These were previously part of Application.js but have been moved out
4
+ */
5
+
2
6
  import { Mouse, Resize, Raf, Scroll } from "./events"
3
7
  import kitStore from "./kitStore"
4
8
  import { sniffer } from "./utils"
package/kitStore.js CHANGED
@@ -1,13 +1,13 @@
1
- //kitStore.js
1
+ // kitStore.js
2
+
2
3
  export const sizes = {
3
4
  vw: 0,
4
5
  vh: 0,
5
6
  fh: 0,
6
- d: 1440, // design width
7
- m: 390, // mobile width
7
+ d: 1440,
8
+ m: 390,
8
9
  }
9
10
 
10
- // in case you want to "cache" your assets
11
11
  export const assets = {}
12
12
 
13
13
  export const flags = {
@@ -32,11 +32,14 @@ export const mouse = {
32
32
  y: 0,
33
33
  }
34
34
 
35
- export default {
35
+ /**
36
+ * Single canonical store object.
37
+ */
38
+ export const kitStore = {
36
39
  sizes,
37
40
  breakpoints,
38
41
  mouse,
39
42
  flags,
40
- pageContent: null, // This should be set to the main content element
43
+ pageContent: null,
41
44
  assets,
42
45
  }
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@linear_non/stellar-kit",
3
- "version": "2.1.21",
3
+ "version": "3.0.1",
4
4
  "description": "Stellar frontend core for Non-Linear Studio projects.",
5
- "main": "index.js",
5
+ "main": "/core/index.js",
6
6
  "exports": {
7
- ".": "./index.js",
7
+ ".": "./kitStore.js",
8
8
  "./utils": "./utils/index.js",
9
+ "./core": "./core/index.js",
9
10
  "./classes": "./classes/index.js",
10
11
  "./events": "./events/index.js",
11
12
  "./plugins": "./plugins/index.js",
@@ -14,10 +15,12 @@
14
15
  "type": "module",
15
16
  "scripts": {
16
17
  "dev": "vite",
18
+ "build": "vite build",
17
19
  "format": "prettier --write .",
18
20
  "check": "vite build --emptyOutDir --watch"
19
21
  },
20
22
  "files": [
23
+ "core/",
21
24
  "classes/",
22
25
  "events/",
23
26
  "plugins/",
package/plugins/Grid.js CHANGED
@@ -1,4 +1,4 @@
1
- import kitStore from "../kitStore"
1
+ import { kitStore } from "../kitStore"
2
2
  import { getWindowSizes } from "../utils/window"
3
3
  import { emitter, EVENTS } from "../events"
4
4
  import { gsap } from "../libraries/gsap"
package/utils/debug.js ADDED
@@ -0,0 +1,64 @@
1
+ import { kitStore } from "../kitStore.js"
2
+
3
+ const COLORS = {
4
+ APP: "#03A9F4",
5
+ ENGINE: "#4CAF50",
6
+ PAGE: "#FF9800",
7
+ MANAGER: "#E91E63",
8
+ ROUTE: "#9C27B0",
9
+ }
10
+
11
+ function fmt(scope) {
12
+ const color = COLORS[scope] || "#999"
13
+ return [`%c[${scope}]`, `color:${color};font-weight:bold`]
14
+ }
15
+
16
+ /**
17
+ * Lightweight global debug helper.
18
+ *
19
+ * - Controlled via kitStore.flags.isDebug
20
+ * - No-op when debug is off
21
+ */
22
+
23
+ export const Debug = {
24
+ enable() {
25
+ kitStore.flags.isDebug = true
26
+ },
27
+
28
+ disable() {
29
+ kitStore.flags.isDebug = false
30
+ },
31
+
32
+ isEnabled() {
33
+ return Boolean(kitStore.flags.isDebug)
34
+ },
35
+
36
+ log(scope, ...args) {
37
+ if (!kitStore.flags.isDebug) return
38
+ const [tag, style] = fmt(scope)
39
+ console.log(tag, style, ...args)
40
+ },
41
+
42
+ warn(scope, ...args) {
43
+ if (!kitStore.flags.isDebug) return
44
+ const [tag, style] = fmt(scope)
45
+ console.warn(tag, style, ...args)
46
+ },
47
+
48
+ error(scope, ...args) {
49
+ if (!kitStore.flags.isDebug) return
50
+ const [tag, style] = fmt(scope)
51
+ console.error(tag, style, ...args)
52
+ },
53
+
54
+ group(scope, fn) {
55
+ if (!kitStore.flags.isDebug) return
56
+ const [tag, style] = fmt(scope)
57
+ console.group(tag, style)
58
+ try {
59
+ fn()
60
+ } finally {
61
+ console.groupEnd()
62
+ }
63
+ },
64
+ }
package/utils/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ export { Debug } from "./debug"
1
2
  export { qs, qsa, getid, gettag } from "./selector"
2
3
  export { bounds } from "./bounds"
3
4
  export { offset } from "./offset"
package/utils/listener.js CHANGED
@@ -1,12 +1,28 @@
1
- // listener.js
1
+ /**
2
+ * @typedef {EventTarget | EventTarget[] | NodeListOf<EventTarget> | HTMLCollectionOf<EventTarget>} ListenerTarget
3
+ */
4
+ /**
5
+ * @typedef {"add" | "remove"} ListenerAction
6
+ */
7
+
8
+ /**
9
+ * Cross-browser helper to add/remove event listeners on a single element or a collection.
10
+ *
11
+ * @param {ListenerTarget} el - Element or collection of elements.
12
+ * @param {ListenerAction} action - `"add"` to add, `"remove"` to remove.
13
+ * @param {string} type - Event type, e.g. `"click"`, `"scroll"`.
14
+ * @param {(event: Event) => void} cb - Event handler callback.
15
+ * @param {boolean} [p] - If `true`, listener is registered as passive.
16
+ * @returns {void}
17
+ */
2
18
  export const listener = (el, action, type, cb, p) => {
3
19
  const passive = p === true ? { passive: true } : false
4
20
 
5
- if (el.length) {
21
+ if (el && "length" in el && typeof el !== "string") {
6
22
  for (let i = 0; i < el.length; i++) {
7
23
  el[i][action + "EventListener"](type, cb, passive)
8
24
  }
9
- } else {
25
+ } else if (el) {
10
26
  el[action + "EventListener"](type, cb, passive)
11
27
  }
12
28
  }
package/utils/math.js CHANGED
@@ -1,16 +1,36 @@
1
- // math.js
2
- export const lerp = (a, b, n) => {
3
- return a * (1 - n) + b * n
4
- }
1
+ /**
2
+ * Linearly interpolates between two numbers.
3
+ * @param {number} a
4
+ * @param {number} b
5
+ * @param {number} n - Normalized factor [0–1]
6
+ * @returns {number}
7
+ */
8
+ export const lerp = (a, b, n) => a * (1 - n) + b * n
5
9
 
6
- export const norm = (val, min, max) => {
7
- return (val - min) / (max - min)
8
- }
10
+ /**
11
+ * Normalizes a value into a 0–1 range.
12
+ * @param {number} val
13
+ * @param {number} min
14
+ * @param {number} max
15
+ * @returns {number}
16
+ */
17
+ export const norm = (val, min, max) => (val - min) / (max - min)
9
18
 
10
- export const clamp = (val, min, max) => {
11
- return Math.min(Math.max(val, min), max)
12
- }
19
+ /**
20
+ * Clamps a number between a minimum and maximum.
21
+ * @param {number} val
22
+ * @param {number} min
23
+ * @param {number} max
24
+ * @returns {number}
25
+ */
26
+ export const clamp = (val, min, max) => Math.min(Math.max(val, min), max)
13
27
 
28
+ /**
29
+ * Rounds a number to a given precision.
30
+ * @param {number} n
31
+ * @param {number} [p=3] - Decimal precision
32
+ * @returns {number}
33
+ */
14
34
  export const round = (n, p) => {
15
35
  const precision = p !== undefined ? Math.pow(10, p) : 1000
16
36
  return Math.round(n * precision) / precision
package/utils/offset.js CHANGED
@@ -1,14 +1,27 @@
1
- // offset.js
2
- // Helpfull to use instead of
3
- // bounds when using smooth scroll
1
+ /**
2
+ * Computes the cumulative offset of an element relative to the document.
3
+ * Use this instead of `bounds()` when smooth-scroll systems shift the viewport.
4
+ *
5
+ * @param {HTMLElement} el
6
+ * @returns {{left: number, top: number, bottom: number}}
7
+ */
4
8
  export const offset = el => {
5
- let left = 0,
6
- top = 0
7
- const node = el
9
+ if (!el) return { left: 0, top: 0, bottom: 0 }
10
+
11
+ let left = 0
12
+ let top = 0
13
+ const base = el
14
+
15
+ /** Walk up the offset parent chain */
8
16
  while (el) {
9
- left += el.offsetLeft
10
- top += el.offsetTop
17
+ left += el.offsetLeft || 0
18
+ top += el.offsetTop || 0
11
19
  el = el.offsetParent
12
20
  }
13
- return { left, top, bottom: top + node.offsetHeight }
21
+
22
+ return {
23
+ left,
24
+ top,
25
+ bottom: top + base.offsetHeight,
26
+ }
14
27
  }
package/utils/selector.js CHANGED
@@ -1,5 +1,35 @@
1
- // selector.js
2
- export const qs = (s, o = document) => o.querySelector(s)
3
- export const qsa = (s, o = document) => Array.from(o.querySelectorAll(s))
4
- export const getid = (s, o = document) => o.getElementById(s)
5
- export const gettag = (s, o = document) => o.getElementsByTagName(s)
1
+ /**
2
+ * Query a single DOM element.
3
+ *
4
+ * @param {string} selector - CSS selector
5
+ * @param {ParentNode} [scope=document] - Scope to search within
6
+ * @returns {Element|null}
7
+ */
8
+ export const qs = (selector, scope = document) => scope.querySelector(selector)
9
+
10
+ /**
11
+ * Query multiple DOM elements.
12
+ *
13
+ * @param {string} selector - CSS selector
14
+ * @param {ParentNode} [scope=document] - Scope to search within
15
+ * @returns {Element[]} - Array of matched elements
16
+ */
17
+ export const qsa = (selector, scope = document) => Array.from(scope.querySelectorAll(selector))
18
+
19
+ /**
20
+ * Get an element by ID.
21
+ *
22
+ * @param {string} id - Element ID
23
+ * @param {Document|HTMLElement} [scope=document] - Scope to search within
24
+ * @returns {HTMLElement|null}
25
+ */
26
+ export const getid = (id, scope = document) => scope.getElementById(id)
27
+
28
+ /**
29
+ * Get elements by tag name.
30
+ *
31
+ * @param {string} tag - Tag name
32
+ * @param {Document|HTMLElement} [scope=document] - Scope to search within
33
+ * @returns {HTMLCollectionOf<Element>}
34
+ */
35
+ export const gettag = (tag, scope = document) => scope.getElementsByTagName(tag)