@linear_non/stellar-kit 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/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # stellar-kit
2
+
3
+ A modular frontend utility kit for custom websites. Built for performance, extensibility, and clean developer ergonomics โ€” with support for smooth scrolling, events, tick updates, mouse tracking, and layout helpers.
4
+
5
+ ## โœจ Features
6
+
7
+ - RAF-based update loop (`Raf`)
8
+ - Virtual scroll support (`Scroll`)
9
+ - Smooth scrolling with transformable sections (`Smooth`)
10
+ - Viewport-based resize tracking (`Resize`)
11
+ - Custom scrollbar component
12
+ - Centralized event system (`Emitter`)
13
+ - Mouse tracking (`Mouse`)
14
+ - Utility helpers (DOM selection, bounds, clamping, etc.)
15
+ - Designed for use with Astro, GSAP, Three.js, and modular frontend setups
16
+
17
+ ## ๐Ÿ“ฆ Installation
18
+
19
+ ```bash
20
+ npm install stellar-kit
21
+ # Or from GitHub:
22
+ npm install nonlinearstudio/stellar-kit
23
+ ```
24
+
25
+ ## ๐Ÿงฑ Folder Structure
26
+
27
+ ```
28
+ stellar-kit/
29
+ โ”œโ”€โ”€ classes/ # Core logic: Scroll, Smooth, Mouse, Scrollbar, etc.
30
+ โ”œโ”€โ”€ events/ # Emitter system: Raf, Resize, Scroll, Mouse, etc.
31
+ โ”œโ”€โ”€ utils/ # Utilities: bounds, clamp, selectors, etc.
32
+ โ”œโ”€โ”€ kitStore.js # Central store shared across all modules
33
+ โ””โ”€โ”€ index.js # Entry point for setupKit and kitStore access
34
+ ```
35
+
36
+ ## ๐Ÿงช Local Development
37
+
38
+ ```bash
39
+ npm install
40
+ npm run dev
41
+ ```
42
+
43
+ > The `dev/` folder provides a Vite playground to test modules like Smooth, Scrollbar, and events. You can import any part of the kit and prototype in isolation.
44
+
45
+ ## ๐Ÿ› ๏ธ Usage Example
46
+
47
+ ```js
48
+ // main.js or index.js of your project
49
+ import { kitStore, setupKit } from "stellar-kit"
50
+
51
+ setupKit() // Initializes Resize, Raf, Scroll, Mouse, etc.
52
+
53
+ // Access anywhere
54
+ kitStore.scroll
55
+ kitStore.raf
56
+ kitStore.mouse
57
+ ```
58
+
59
+ You can also import from specific paths:
60
+
61
+ ```js
62
+ import { bounds, clamp, qs } from "stellar-kit/utils"
63
+ import { Manager } from "stellar-kit/classes"
64
+ import { Raf } from "stellar-kit/events"
65
+ ```
66
+
67
+ ---
68
+
69
+ Made with โค๏ธ by [Non-Linear Studio](https://non-linear.studio)
@@ -0,0 +1,46 @@
1
+ // Component.js
2
+ import { qs, bounds } from "../utils"
3
+
4
+ export default class Component {
5
+ constructor(obj) {
6
+ this.el = obj.el || null
7
+ this.renderer = obj.renderer || null
8
+ const id = obj.index || 0
9
+ const data = obj.data || null
10
+
11
+ this.hasTick = data?.hasTick || false
12
+ this.hasAnimateIn = data?.hasAnimateIn || false
13
+ this.hasAnimateOut = data?.hasAnimateOut || false
14
+ this.hasAnimateOnScroll = data?.hasAnimateOnScroll || false
15
+
16
+ if (this.el) this.rect = bounds(this.el)
17
+ }
18
+
19
+ setup() {
20
+ if (!this.name) return
21
+ this.el = qs(`[data-${this.name}]`)
22
+ }
23
+
24
+ animateIn() {}
25
+ animateOut() {}
26
+ animateOnScroll() {}
27
+
28
+ tick(obj) {}
29
+ resize() {}
30
+ smoothResize() {}
31
+
32
+ on() {}
33
+ off() {}
34
+
35
+ destroy() {
36
+ this.off()
37
+ }
38
+
39
+ setWebgl() {}
40
+
41
+ render() {
42
+ this.setup()
43
+ this.on()
44
+ if (this.hasAnimateOnScroll) this.animateOnScroll()
45
+ }
46
+ }
@@ -0,0 +1,73 @@
1
+ // Manager.js
2
+ import { qsa } from "../utils"
3
+
4
+ export default class Manager {
5
+ constructor() {
6
+ this.components = []
7
+ }
8
+
9
+ addComponent(obj, renderer) {
10
+ const { name, instance } = obj
11
+ const els = qsa(`[data-${name}]`)
12
+ const data = {}
13
+ if (els.length === 0) return
14
+
15
+ data.hasTick = obj.hasTick || false
16
+ data.hasAnimateIn = obj.hasAnimateIn || false
17
+ data.hasAnimateOut = obj.hasAnimateOut || false
18
+ data.hasAnimateOnScroll = obj.hasAnimateOnScroll || false
19
+
20
+ els.forEach((el, index) => {
21
+ const component = new instance({ index, el, data, renderer })
22
+ this.components.push({ name, component })
23
+ })
24
+ }
25
+
26
+ initializeComponents() {
27
+ this.components.forEach(({ component }) => {
28
+ component?.render()
29
+ })
30
+ }
31
+
32
+ animateIn() {
33
+ this.components.forEach(({ component }) => {
34
+ component.hasAnimateIn && component.animateIn()
35
+ })
36
+ }
37
+
38
+ animateOut() {
39
+ this.components.forEach(({ component }) => {
40
+ component.hasAnimateOut && component.animateOut()
41
+ })
42
+ }
43
+
44
+ animateOnScroll() {
45
+ this.components.forEach(({ component }) => {
46
+ component.hasAnimateOnScroll && component.animateOnScroll()
47
+ })
48
+ }
49
+
50
+ update(obj) {
51
+ this.components.forEach(({ component }) => {
52
+ component.hasTick && component.tick(obj)
53
+ })
54
+ }
55
+
56
+ resize() {
57
+ this.components.forEach(({ component }) => {
58
+ component?.resize()
59
+ })
60
+ }
61
+
62
+ smoothResize() {
63
+ this.components.forEach(({ component }) => {
64
+ component?.smoothResize()
65
+ })
66
+ }
67
+
68
+ destroy() {
69
+ this.components.forEach(({ component }) => {
70
+ component?.destroy()
71
+ })
72
+ }
73
+ }
@@ -0,0 +1,124 @@
1
+ import kitStore from "../kitStore"
2
+ import { emitter, EVENTS, Raf } from "../events"
3
+
4
+ export default class Scrollbar {
5
+ constructor(obj) {
6
+ this.container = obj.container || document.body
7
+ this.el = null
8
+ this.handle = null
9
+ this.touching = false
10
+
11
+ this.state = {
12
+ clicked: false,
13
+ scale: 0,
14
+ current: 0,
15
+ }
16
+ this.height = 0
17
+ this.init()
18
+ }
19
+
20
+ init() {
21
+ this.create()
22
+ this.setBounds()
23
+ this.on()
24
+ }
25
+
26
+ on() {
27
+ emitter.on(EVENTS.APP_TICK, this.tick)
28
+ emitter.on(EVENTS.APP_RESIZE, this.resize)
29
+ this.handle.addEventListener("mousedown", this.mouseDown)
30
+ window.addEventListener("mouseup", this.mouseUp)
31
+ window.addEventListener("mousemove", this.mouseMove)
32
+ }
33
+
34
+ off() {
35
+ emitter.off(EVENTS.APP_TICK, this.tick)
36
+ emitter.off(EVENTS.APP_RESIZE, this.resize)
37
+ this.handle.removeEventListener("mousedown", this.mouseDown)
38
+ window.removeEventListener("mouseup", this.mouseUp)
39
+ window.removeEventListener("mousemove", this.mouseMove)
40
+ }
41
+
42
+ mouseDown = () => {
43
+ this.touching = true
44
+ document.body.style.userSelect = "none"
45
+ document.body.style.webkitUserSelect = "none"
46
+ document.body.style.pointerEvents = "none"
47
+ }
48
+
49
+ mouseUp = () => {
50
+ this.touching = false
51
+ document.body.style.userSelect = "inherit"
52
+ document.body.style.webkitUserSelect = "inherit"
53
+ document.body.style.pointerEvents = "inherit"
54
+ }
55
+
56
+ mouseMove = e => {
57
+ if (this.touching === true) {
58
+ const { isLocked } = kitStore.flags
59
+
60
+ if (isLocked) return
61
+
62
+ kitStore.raf.scroll.target = e.clientY * this.state.scale
63
+ kitStore.raf.clamp()
64
+ }
65
+ }
66
+
67
+ setBounds() {
68
+ const { vh, fh } = kitStore.sizes
69
+ const scrollLimit = fh
70
+ this.state.scale = (scrollLimit + vh) / vh
71
+ this.handle.style.height = `${vh / this.state.scale}px`
72
+ }
73
+
74
+ tick = ({ current }) => {
75
+ const scroll = current / this.state.scale
76
+ this.state.current = scroll
77
+ this.handle.style.transform = `translate3d(0, ${this.state.current}px, 0)`
78
+ }
79
+
80
+ resize = () => {
81
+ this.setBounds()
82
+ }
83
+
84
+ calcScroll(e) {
85
+ const delta = e.clientY * this.state.scale
86
+ Raf.target = delta
87
+ Raf.clampTarget()
88
+ }
89
+
90
+ create() {
91
+ this.el = document.createElement("div")
92
+ this.handle = document.createElement("div")
93
+ this.el.classList.add("scrollbar")
94
+ this.handle.classList.add("handle")
95
+
96
+ Object.assign(this.el.style, {
97
+ position: "fixed",
98
+ top: 0,
99
+ right: 0,
100
+ height: "100%",
101
+ pointerEvents: "all",
102
+ })
103
+
104
+ Object.assign(this.handle.style, {
105
+ position: "absolute",
106
+ top: 0,
107
+ left: 0,
108
+ width: "100%",
109
+ cursor: "pointer",
110
+ zIndex: 101,
111
+ })
112
+
113
+ this.container.appendChild(this.el)
114
+ this.el.appendChild(this.handle)
115
+ }
116
+
117
+ update() {
118
+ this.setBounds()
119
+ }
120
+
121
+ destroy() {
122
+ this.off()
123
+ }
124
+ }
@@ -0,0 +1,146 @@
1
+ // Smooth.js
2
+ import kitStore from "../kitStore"
3
+ import Scrollbar from "./Scrollbar"
4
+ import { emitter, EVENTS } from "../events"
5
+ import { qs, qsa, bounds } from "../utils"
6
+
7
+ export default class Smooth {
8
+ constructor(obj) {
9
+ this.scroll = obj.scroll
10
+ this.el = qs("[data-view]")
11
+ this.elems = null
12
+ this.current = 0
13
+ this.threshold = 100
14
+ this.sections = null
15
+ this.scrollbar = null
16
+ this.init()
17
+ }
18
+
19
+ getSections() {
20
+ const { isSmooth } = kitStore.flags
21
+ if (!this.elems || !isSmooth) return
22
+
23
+ this.sections = []
24
+ this.elems.forEach(el => {
25
+ el.style.transform = "translate3d(0, 0, 0)"
26
+ const speed = el.dataset.speed || 1
27
+ const { top, bottom, offset } = this.getVars(el, speed)
28
+ let parent = el.parentNode.closest("[data-smooth]")
29
+
30
+ if (parent) {
31
+ this.sections.some(obj => {
32
+ if (obj.el === parent) parent = obj
33
+ })
34
+ }
35
+
36
+ this.sections.push({
37
+ el,
38
+ parent,
39
+ top,
40
+ bottom,
41
+ offset,
42
+ speed,
43
+ out: true,
44
+ transform: 0,
45
+ })
46
+ })
47
+ }
48
+
49
+ tick = ({ current }) => {
50
+ this.current = current
51
+ this.transformSections()
52
+ }
53
+
54
+ transformSections() {
55
+ const { isSmooth, isResizing } = kitStore.flags
56
+ if (!this.sections || !isSmooth) return
57
+
58
+ this.sections.forEach(section => {
59
+ const { isVisible, transform } = this.isVisible(section)
60
+
61
+ if (isVisible || isResizing || !section.out) {
62
+ section.out = !isVisible
63
+ section.transform = transform
64
+ section.el.style.transform = this.getTransform(transform)
65
+ }
66
+ })
67
+ }
68
+
69
+ isVisible(section) {
70
+ const { vh } = kitStore.sizes
71
+ const { top, bottom, offset, speed, parent } = section
72
+
73
+ const extra = (parent && parent.transform) || 0
74
+ const translate = this.current * speed
75
+ const transform = translate - offset - extra
76
+ const start = top - translate
77
+ const end = bottom - translate
78
+ const isVisible = start < this.threshold + vh && end > -this.threshold
79
+
80
+ return { isVisible, transform }
81
+ }
82
+
83
+ getTransform(transform) {
84
+ return `translate3d(0, ${-transform}px, 0)`
85
+ }
86
+
87
+ getVars(el, speed) {
88
+ const { vh } = kitStore.sizes
89
+ const rect = bounds(el)
90
+ const centering = vh / 2 - rect.height / 2
91
+ const offset = rect.top < vh ? 0 : (rect.top - centering) * speed - (rect.top - centering)
92
+
93
+ return {
94
+ top: rect.top + offset,
95
+ bottom: rect.bottom + offset,
96
+ offset,
97
+ }
98
+ }
99
+
100
+ resize = () => {
101
+ if (!this.sections) return
102
+
103
+ this.sections.forEach(section => {
104
+ section.el.style.transform = "translate3d(0, 0, 0)"
105
+ const { top, bottom, offset } = this.getVars(section.el, section.speed)
106
+ Object.assign(section, { top, bottom, offset })
107
+ })
108
+
109
+ emitter.emit(EVENTS.APP_SMOOTH_RESIZE)
110
+ this.transformSections()
111
+ }
112
+
113
+ update(elems) {
114
+ kitStore.flags.isResizing = true
115
+
116
+ this.scroll.setScrollBounds()
117
+ this.elems = elems || qsa("[data-smooth]")
118
+ this.scrollbar.update()
119
+ this.getSections()
120
+ this.transformSections()
121
+
122
+ kitStore.flags.isResizing = false
123
+ }
124
+
125
+ clean() {
126
+ this.elems = this.sections = null
127
+ }
128
+
129
+ on() {
130
+ emitter.on(EVENTS.APP_TICK, this.tick)
131
+ emitter.on(EVENTS.APP_RESIZE, this.resize)
132
+ }
133
+
134
+ off() {
135
+ emitter.off(EVENTS.APP_TICK, this.tick)
136
+ emitter.off(EVENTS.APP_RESIZE, this.resize)
137
+ }
138
+
139
+ init(elems) {
140
+ this.elems = elems || qsa("[data-smooth]")
141
+ this.getSections()
142
+ this.scrollbar = new Scrollbar(this.current)
143
+ this.on()
144
+ this.update()
145
+ }
146
+ }
@@ -0,0 +1,6 @@
1
+ import Component from "./Component"
2
+ import Manager from "./Manager"
3
+ import Smooth from "./Smooth"
4
+ import Scrollbar from "./Scrollbar"
5
+
6
+ export { Component, Manager, Smooth, Scrollbar }
@@ -0,0 +1,67 @@
1
+ // Emitter.js
2
+ class Emitter {
3
+ constructor() {
4
+ this.events = {}
5
+ }
6
+
7
+ emit(event, ...args) {
8
+ const callbacks = this.events[event] || []
9
+ for (let i = 0, { length } = callbacks; i < length; i++) {
10
+ callbacks[i].cb(...args)
11
+ }
12
+ }
13
+
14
+ on(event, cb, priority = 0) {
15
+ const data = { cb, priority }
16
+ this.events[event]?.push(data) || (this.events[event] = [data])
17
+ this.events[event].sort((a, b) => a.priority - b.priority)
18
+ return () => {
19
+ this.events[event] = this.events[event]?.filter(v => cb !== v.cb)
20
+ }
21
+ }
22
+
23
+ off(event, callback) {
24
+ this.events[event] = this.events[event]?.filter(({ cb }) => callback !== cb)
25
+ }
26
+
27
+ once(event, cb, priority = 0) {
28
+ const onceCallback = (...args) => {
29
+ cb(...args)
30
+ this.off(event, onceCallback)
31
+ }
32
+
33
+ this.on(event, onceCallback, priority)
34
+
35
+ return () => {
36
+ this.off(event, cb)
37
+ }
38
+ }
39
+
40
+ destroy() {
41
+ this.events = {}
42
+ }
43
+ }
44
+
45
+ const emitter = new Emitter()
46
+
47
+ const EVENTS = {
48
+ APP_TICK: "tick",
49
+ APP_RESIZE: "resize",
50
+ APP_SMOOTH_RESIZE: "smooth:resize",
51
+ APP_SCROLL: "scroll",
52
+ APP_MOUSEMOVE: "mousemove",
53
+ APP_MOUSEDOWN: "mousedown",
54
+ APP_MOUSEUP: "mouseup",
55
+ APP_SPLITTEXT_READY: "splittext:ready",
56
+ }
57
+
58
+ const PRIORITY = {
59
+ first: -10,
60
+ instant: 0,
61
+ high: 10,
62
+ mid: 20,
63
+ low: 30,
64
+ }
65
+
66
+ export { EVENTS, PRIORITY }
67
+ export default emitter
@@ -0,0 +1,60 @@
1
+ // Mouse.js
2
+ import emitter, { EVENTS } from "./Emitter"
3
+ import { sniffer } from "../utils"
4
+
5
+ export default class Mouse {
6
+ constructor() {
7
+ this.state = {
8
+ on: 0,
9
+ off: 0,
10
+ coords: { x: 0, y: 0 },
11
+ }
12
+
13
+ this.events = {
14
+ move: sniffer.isDevice ? "touchmove" : "mousemove",
15
+ down: sniffer.isDevice ? "touchstart" : "mousedown",
16
+ up: sniffer.isDevice ? "touchend" : "mouseup",
17
+ }
18
+
19
+ this.on()
20
+ }
21
+
22
+ on() {
23
+ const { move, down, up } = this.events
24
+ window.addEventListener(move, this.onMove)
25
+ window.addEventListener(down, this.onDown)
26
+ window.addEventListener(up, this.onUp)
27
+ }
28
+
29
+ off() {
30
+ const { move, down, up } = this.events
31
+ window.removeEventListener(move, this.onMove)
32
+ window.removeEventListener(down, this.onDown)
33
+ window.removeEventListener(up, this.onUp)
34
+ }
35
+
36
+ getPos(e) {
37
+ const x = e.changedTouches ? e.changedTouches[0].clientX : e.clientX
38
+ const y = e.changedTouches ? e.changedTouches[0].clientY : e.clientY
39
+ const target = e.target
40
+ return { x, y, target }
41
+ }
42
+
43
+ onMove = e => {
44
+ const { x, y, target } = this.getPos(e)
45
+ emitter.emit(EVENTS.APP_MOUSEMOVE, { x, y, target, e })
46
+ }
47
+
48
+ onDown = e => {
49
+ const { x, y, target } = this.getPos(e)
50
+ this.state.on = x
51
+ emitter.emit(EVENTS.APP_MOUSEDOWN, { x, y, target })
52
+ }
53
+
54
+ onUp = e => {
55
+ const { x, target } = this.getPos(e)
56
+ this.state.off = x
57
+ const isClick = Math.abs(this.state.on - this.state.off) > 10
58
+ emitter.emit(EVENTS.APP_MOUSEUP, { x, target, isClick })
59
+ }
60
+ }
package/events/Raf.js ADDED
@@ -0,0 +1,122 @@
1
+ // Raf.js
2
+ import kitStore from "../kitStore"
3
+ import emitter, { EVENTS } from "./Emitter"
4
+ import { sniffer, lerp } from "../utils"
5
+ import { gsap, ScrollTrigger } from "../libraries/gsap"
6
+
7
+ export default class Raf {
8
+ constructor() {
9
+ this.scroll = {
10
+ target: 0,
11
+ current: 0,
12
+ rounded: 0,
13
+ ease: 0.115,
14
+ }
15
+
16
+ this.mouse = {
17
+ x: 0,
18
+ y: 0,
19
+ target: null,
20
+ }
21
+
22
+ this.setScrollTrigger()
23
+ this.on()
24
+ }
25
+
26
+ tick = () => {
27
+ const { target, current, ease } = this.scroll
28
+
29
+ this.scroll.current = lerp(current, target, ease)
30
+ this.scroll.rounded = Math.round(this.scroll.current * 100) / 100
31
+ this.diff = (target - this.scroll.current) * 0.005
32
+
33
+ emitter.emit(EVENTS.APP_TICK, {
34
+ target,
35
+ current: this.getScroll(),
36
+ mouse: this.mouse,
37
+ diff: this.diff,
38
+ })
39
+
40
+ ScrollTrigger.update()
41
+ }
42
+
43
+ getScroll() {
44
+ const { pageContent } = kitStore
45
+ return sniffer.isDevice ? pageContent.parentNode.scrollTop : this.scroll.rounded
46
+ }
47
+
48
+ clamp() {
49
+ const { fh } = kitStore.sizes
50
+ this.scroll.target = Math.min(Math.max(this.scroll.target, 0), fh)
51
+ }
52
+
53
+ onScroll = ({ y }) => {
54
+ if (kitStore.flags.isLocked) return
55
+ this.scroll.target += y
56
+ this.clamp()
57
+ }
58
+
59
+ onMouseMove = ({ x, y, target }) => {
60
+ this.mouse.x = x
61
+ this.mouse.y = y
62
+ this.mouse.target = target
63
+ }
64
+
65
+ scrollTo(offset) {
66
+ if (kitStore.flags.isSmooth) {
67
+ gsap.to(this.scroll, {
68
+ target: offset,
69
+ duration: 1,
70
+ ease: "expo.inOut",
71
+ })
72
+ } else {
73
+ window.scrollTo({ top: offset, left: 0, behavior: "smooth" })
74
+ }
75
+ }
76
+
77
+ setScroll(offset) {
78
+ if (kitStore.flags.isSmooth) {
79
+ gsap.set(this.scroll, {
80
+ target: offset,
81
+ current: offset,
82
+ rounded: offset,
83
+ })
84
+ } else {
85
+ window.scrollTo({ top: offset, left: 0 })
86
+ }
87
+ }
88
+
89
+ setScrollTrigger() {
90
+ const { pageContent } = kitStore
91
+
92
+ ScrollTrigger.defaults({
93
+ scroller: pageContent,
94
+ })
95
+
96
+ ScrollTrigger.scrollerProxy(pageContent, {
97
+ scrollTop: () => {
98
+ return this.smooth()
99
+ },
100
+ getBoundingClientRect() {
101
+ // Important that width and height are dynamic
102
+ return { top: 0, left: 0, width: window.innerWidth, height: window.innerHeight }
103
+ },
104
+ })
105
+ }
106
+
107
+ on() {
108
+ gsap.ticker.add(this.tick)
109
+ emitter.on(EVENTS.APP_SCROLL, this.onScroll)
110
+ emitter.on(EVENTS.APP_MOUSEMOVE, this.onMouseMove)
111
+ }
112
+
113
+ off() {
114
+ gsap.ticker.remove(this.tick)
115
+ emitter.off(EVENTS.APP_SCROLL, this.onScroll)
116
+ emitter.off(EVENTS.APP_MOUSEMOVE, this.onMouseMove)
117
+ }
118
+
119
+ destroy() {
120
+ this.off()
121
+ }
122
+ }