@linear_non/stellar-kit 1.1.1 → 1.1.3

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.
@@ -0,0 +1,47 @@
1
+ // FontLoader.js
2
+ import { emitter, EVENTS } from "../events"
3
+
4
+ export default class FontLoader {
5
+ static type = "fonts"
6
+ static events = { PROGRESS: EVENTS.APP_FONTS_PROGRESS, LOADED: EVENTS.APP_FONTS_LOADED }
7
+ constructor({ fonts = [] }) {
8
+ // fonts = [{ family: "Inter", url: "/fonts/Inter.woff2" }, ...]
9
+
10
+ this.fonts = fonts
11
+ this.loaded = 0
12
+ this.total = fonts.length
13
+ this.isLoaded = false
14
+ this.items = []
15
+ }
16
+
17
+ async load() {
18
+ const promises = this.fonts.map(font => this.loadSingle(font))
19
+ await Promise.all(promises)
20
+
21
+ this.isLoaded = true
22
+ emitter.emit(EVENTS.APP_FONTS_LOADED, this.items)
23
+ return this.items
24
+ }
25
+
26
+ async loadSingle({ family, url, descriptors = {} }) {
27
+ const font = new FontFace(family, `url(${url})`, descriptors)
28
+
29
+ try {
30
+ const loadedFace = await font.load()
31
+ document.fonts.add(loadedFace)
32
+ this.items.push(loadedFace)
33
+ this.loaded++
34
+
35
+ emitter.emit(EVENTS.APP_FONTS_PROGRESS, {
36
+ loaded: this.loaded,
37
+ total: this.total,
38
+ percent: Math.round((this.loaded / this.total) * 100),
39
+ })
40
+
41
+ return loadedFace
42
+ } catch (err) {
43
+ console.error("FontLoader error:", err)
44
+ return null
45
+ }
46
+ }
47
+ }
@@ -0,0 +1,60 @@
1
+ // ImageLoader.js
2
+ import { emitter, EVENTS } from "../events"
3
+
4
+ export default class ImageLoader {
5
+ static type = "images"
6
+ static events = { PROGRESS: EVENTS.APP_IMAGES_PROGRESS, LOADED: EVENTS.APP_IMAGES_LOADED }
7
+ constructor({ origin, fileName, extension, total, urls = [] }) {
8
+ this.origin = origin
9
+ this.fileName = fileName
10
+ this.extension = extension
11
+ this.total = total
12
+ this.urls = urls
13
+
14
+ this.images = []
15
+ this.loaded = 0
16
+ this.isLoaded = false
17
+ }
18
+
19
+ async load() {
20
+ // If explicit URLs are passed, use them
21
+ const urls = this.urls.length
22
+ ? this.urls
23
+ : Array.from({ length: this.total }, (_, i) => `${this.origin}/${this.fileName}${i}.${this.extension}`)
24
+
25
+ this.total = urls.length
26
+
27
+ const promises = urls.map((url, i) => this.loadSingle(url, i))
28
+ await Promise.all(promises)
29
+
30
+ this.isLoaded = true
31
+ emitter.emit(EVENTS.APP_IMAGES_LOADED, this.images)
32
+ }
33
+
34
+ loadSingle(url, index) {
35
+ return new Promise(resolve => {
36
+ const worker = new Worker(new URL("../workers/image-loader.js", import.meta.url))
37
+ worker.postMessage({ url, index })
38
+
39
+ worker.addEventListener("message", e => {
40
+ const { blob, index } = e.data
41
+ const img = new Image()
42
+ img.src = URL.createObjectURL(blob)
43
+
44
+ img.onload = () => {
45
+ this.images[index] = img
46
+ this.loaded++
47
+
48
+ emitter.emit(EVENTS.APP_IMAGES_PROGRESS, {
49
+ loaded: this.loaded,
50
+ total: this.total,
51
+ percent: Math.round((this.loaded / this.total) * 100),
52
+ })
53
+
54
+ resolve(img)
55
+ worker.terminate()
56
+ }
57
+ })
58
+ })
59
+ }
60
+ }
@@ -0,0 +1,50 @@
1
+ //MasterLoader.js
2
+ import { emitter, EVENTS } from "../events"
3
+
4
+ export default class MasterLoader {
5
+ constructor() {
6
+ this.loaders = []
7
+ this.results = {}
8
+ this.total = 0
9
+ this.loaded = 0
10
+ }
11
+
12
+ add(loaderClass, options) {
13
+ const loader = new loaderClass(options)
14
+ this.loaders.push({ loaderClass, loader })
15
+ this.total += loader.total || 0
16
+ return loader
17
+ }
18
+
19
+ async loadAll() {
20
+ const promises = this.loaders.map(({ loaderClass, loader }) => {
21
+ return new Promise(resolve => {
22
+ const onProgress = () => {
23
+ this.loaded++
24
+ emitter.emit(EVENTS.APP_MASTER_PROGRESS, {
25
+ loaded: this.loaded,
26
+ total: this.total,
27
+ percent: Math.round((this.loaded / this.total) * 100),
28
+ })
29
+ }
30
+
31
+ const onLoaded = items => {
32
+ const type = loaderClass.type
33
+ this.results[type] = (this.results[type] || []).concat(items)
34
+ // cleanup
35
+ emitter.off?.(loaderClass.events.PROGRESS, onProgress)
36
+ emitter.off?.(loaderClass.events.LOADED, onLoaded)
37
+ resolve()
38
+ }
39
+
40
+ emitter.on(loaderClass.events.PROGRESS, onProgress)
41
+ emitter.on(loaderClass.events.LOADED, onLoaded)
42
+
43
+ loader.load()
44
+ })
45
+ })
46
+
47
+ await Promise.all(promises)
48
+ emitter.emit(EVENTS.APP_MASTER_LOADED, this.results)
49
+ }
50
+ }
package/classes/index.js CHANGED
@@ -1,4 +1,7 @@
1
1
  import Component from "./Component"
2
2
  import Manager from "./Manager"
3
+ import MasterLoader from "./MasterLoader"
4
+ import ImageLoader from "./ImageLoader"
5
+ import FontLoader from "./FontLoader"
3
6
 
4
- export { Component, Manager }
7
+ export { Component, Manager, MasterLoader, ImageLoader, FontLoader }
package/events/Emitter.js CHANGED
@@ -53,6 +53,12 @@ const EVENTS = {
53
53
  APP_MOUSEDOWN: "mousedown",
54
54
  APP_MOUSEUP: "mouseup",
55
55
  APP_SPLITTEXT_READY: "splittext:ready",
56
+ APP_IMAGES_LOADED: "images:loaded",
57
+ APP_IMAGES_PROGRESS: "images:progress",
58
+ APP_MASTER_LOADED: "master:loaded",
59
+ APP_MASTER_PROGRESS: "master:progress",
60
+ APP_FONTS_PROGRESS: "fonts:progress",
61
+ APP_FONTS_LOADED: "fonts:loaded",
56
62
  }
57
63
 
58
64
  const PRIORITY = {
package/index.js CHANGED
@@ -13,4 +13,14 @@ export function setupKit({ isSmooth = sniffer.isDesktop } = {}) {
13
13
  kitStore.mouse = new Mouse()
14
14
  }
15
15
 
16
+ export function setSizes(d = kitStore.sizes.d, m = kitStore.sizes.m) {
17
+ // update store if values are provided
18
+ kitStore.sizes.d = d
19
+ kitStore.sizes.m = m
20
+
21
+ // set css vars
22
+ document.documentElement.style.setProperty("--desktop", d)
23
+ document.documentElement.style.setProperty("--mobile", m)
24
+ }
25
+
16
26
  export { kitStore }
package/kitStore.js CHANGED
@@ -3,6 +3,8 @@ export const sizes = {
3
3
  vw: 0,
4
4
  vh: 0,
5
5
  fh: 0,
6
+ d: 1440, // design width
7
+ m: 390, // mobile width
6
8
  }
7
9
 
8
10
  export const flags = {
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@linear_non/stellar-kit",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "description": "Stellar frontend core for Non-Linear Studio projects.",
5
5
  "main": "index.js",
6
6
  "exports": {
7
7
  ".": "./index.js",
8
8
  "./utils": "./utils/index.js",
9
9
  "./classes": "./classes/index.js",
10
+ "./workers": "./workers/*",
10
11
  "./events": "./events/index.js",
11
12
  "./gsap": "./libraries/gsap/index.js"
12
13
  },
@@ -22,6 +23,7 @@
22
23
  "utils/",
23
24
  "libraries/",
24
25
  "libraries/gsap/",
26
+ "workers/",
25
27
  "kitStore.js",
26
28
  "index.js"
27
29
  ],
package/utils/grid.js ADDED
@@ -0,0 +1,154 @@
1
+ import kitStore from "../kitStore"
2
+ import { getWindowSizes } from "./window"
3
+ import { emitter, EVENTS } from "../events"
4
+ import { gsap } from "../libraries/gsap"
5
+
6
+ export default class Grid {
7
+ constructor(obj) {
8
+ this.d = obj.desktop
9
+ this.m = obj.mobile
10
+ this.state = { toggle: false }
11
+ this.sizes = getWindowSizes()
12
+
13
+ this.handleKeyDown = e => this.onVisibilityToggle(e)
14
+
15
+ this.build()
16
+ this.on()
17
+ }
18
+
19
+ build() {
20
+ this.el = document.createElement("div")
21
+ this.el.classList.add("-grid")
22
+ Object.assign(this.el.style, {
23
+ position: "fixed",
24
+ zIndex: "99999",
25
+ height: "100%",
26
+ inset: "0",
27
+ pointerEvents: "none",
28
+ width: "100%",
29
+ opacity: "0",
30
+ visibility: "hidden",
31
+ })
32
+
33
+ this.dContainer = document.createElement("div")
34
+ this.dContainer.classList.add("-grid-desktop")
35
+ this.el.appendChild(this.dContainer)
36
+
37
+ this.mContainer = document.createElement("div")
38
+ this.mContainer.classList.add("-grid-mobile")
39
+ this.el.appendChild(this.mContainer)
40
+
41
+ const flexFull = {
42
+ justifyContent: "center",
43
+ alignItems: "stretch",
44
+ boxSizing: "border-box",
45
+ height: "100%",
46
+ width: "100%",
47
+ gap: "0",
48
+ display: "flex",
49
+ }
50
+ Object.assign(this.dContainer.style, flexFull)
51
+ Object.assign(this.mContainer.style, flexFull)
52
+
53
+ this.gridSetup(this.dContainer, true)
54
+ this.gridSetup(this.mContainer, false)
55
+ this.applyBreakpointVisibility()
56
+ }
57
+
58
+ attach() {
59
+ if (!this.el.isConnected) document.body.appendChild(this.el)
60
+ }
61
+ detach() {
62
+ if (this.el?.isConnected) this.el.remove()
63
+ }
64
+
65
+ gridSetup(container, isDesktop = true) {
66
+ const { wrap, gap, count } = isDesktop ? this.d : this.m
67
+ const col = this.checkCols(wrap, gap, count, isDesktop)
68
+
69
+ container.innerHTML = ""
70
+ if (wrap > 0) {
71
+ container.style.setProperty("padding-left", `${wrap / 10}rem`)
72
+ container.style.setProperty("padding-right", `${wrap / 10}rem`)
73
+ }
74
+ container.style.setProperty("gap", gap > 0 ? `${gap / 10}rem` : "0")
75
+
76
+ const frag = document.createDocumentFragment()
77
+ for (let i = 0; i < count; i++) {
78
+ const child = document.createElement("div")
79
+ child.classList.add("row")
80
+ Object.assign(child.style, {
81
+ background: "red",
82
+ width: `${col / 10}rem`,
83
+ height: "100%",
84
+ opacity: i % 2 === 0 ? "0.4" : "0.6",
85
+ })
86
+ frag.appendChild(child)
87
+ }
88
+ container.appendChild(frag)
89
+ }
90
+
91
+ checkCols(wrap, gap, count, isDesktop = true) {
92
+ const { sizes } = kitStore
93
+ const base = isDesktop ? sizes.d : sizes.m
94
+ const w = wrap * 2
95
+ const g = (count - 1) * gap
96
+ return (base - w - g) / count
97
+ }
98
+
99
+ applyBreakpointVisibility() {
100
+ const isDesktop = this.sizes.M_UP
101
+ this.dContainer.style.display = isDesktop ? "flex" : "none"
102
+ this.mContainer.style.display = isDesktop ? "none" : "flex"
103
+ }
104
+
105
+ rebuildForBreakpoint() {
106
+ this.applyBreakpointVisibility()
107
+ if (this.sizes.M_UP) {
108
+ this.gridSetup(this.dContainer, true)
109
+ } else {
110
+ this.gridSetup(this.mContainer, false)
111
+ }
112
+ }
113
+
114
+ resize = () => {
115
+ this.sizes = getWindowSizes()
116
+ this.rebuildForBreakpoint()
117
+ }
118
+
119
+ onVisibilityToggle(e) {
120
+ const isCtrlPressed = e.metaKey || e.ctrlKey
121
+ if (e.type !== "keydown" || e.repeat) return
122
+ if (isCtrlPressed && (e.key?.toLowerCase() === "g" || e.code === "KeyG")) {
123
+ if (!this.state.toggle) {
124
+ this.attach()
125
+ gsap.to(this.el, { duration: 0.35, autoAlpha: 1 })
126
+ } else {
127
+ gsap.to(this.el, {
128
+ duration: 0.35,
129
+ autoAlpha: 0,
130
+ onComplete: () => this.detach(),
131
+ })
132
+ }
133
+ this.state.toggle = !this.state.toggle
134
+ }
135
+ }
136
+
137
+ on() {
138
+ document.addEventListener("keydown", this.handleKeyDown)
139
+ emitter.on(EVENTS.APP_RESIZE, this.resize)
140
+ }
141
+
142
+ off() {
143
+ document.removeEventListener("keydown", this.handleKeyDown)
144
+ emitter.off(EVENTS.APP_RESIZE, this.resize)
145
+ }
146
+
147
+ destroy() {
148
+ this.off()
149
+ this.detach()
150
+ this.el = null
151
+ this.dContainer = null
152
+ this.mContainer = null
153
+ }
154
+ }
package/utils/index.js CHANGED
@@ -1,8 +1,11 @@
1
+ import Grid from "./grid"
1
2
  export { qs, qsa, getid, gettag } from "./selector"
2
3
  export { bounds } from "./bounds"
4
+ export { offset } from "./offset"
3
5
  export { getViewport, getWindowSizes, setViewportHeight } from "./window"
4
6
  export { sniffer } from "./sniffer"
5
7
  export { lerp, clamp, norm, round } from "./math"
6
8
  export { supportWebp, supportMouseTouch } from "./support"
7
9
  export { listener } from "./listener"
8
10
  export { splitText, reverseSplit } from "./splitText"
11
+ export { Grid }
@@ -0,0 +1,14 @@
1
+ // offset.js
2
+ // Helpfull to use instead of
3
+ // bounds when using smooth scroll
4
+ export const offset = el => {
5
+ let left = 0,
6
+ top = 0
7
+ const node = el
8
+ while (el) {
9
+ left += el.offsetLeft
10
+ top += el.offsetTop
11
+ el = el.offsetParent
12
+ }
13
+ return { left, top, bottom: top + node.offsetHeight }
14
+ }
@@ -0,0 +1,10 @@
1
+ self.addEventListener("message", async e => {
2
+ const { url, index } = e.data
3
+ try {
4
+ const res = await fetch(url)
5
+ const blob = await res.blob()
6
+ self.postMessage({ url, blob, index })
7
+ } catch (err) {
8
+ console.error("Image worker error:", err)
9
+ }
10
+ })