diff --git a/ui/_/code/quill.js b/ui/_/code/quill.js index fcebb29..1c3c315 100644 --- a/ui/_/code/quill.js +++ b/ui/_/code/quill.js @@ -1,6 +1,8 @@ /* Sam Russell Captured Sun + 12.17.25 - [Hyperia] - adding width, height functions. adding "e" to onClick. adding the non-window $$ funcs. + 12.16.25 - [comalyr] - State 11.25.25.1 - Added minHeight and minWidth to be counted as numerical styles 11.25.25 - Added onChange for check boxes, added setQuery / onQueryChanged for easy filtering 11.24.25 - Fixing onClick because it was reversed, adding event to onHover params @@ -68,6 +70,12 @@ window.$ = function(selector, el = document) { window.$$ = function(selector, el = document) { return Array.from(el.querySelectorAll(selector)) } +HTMLElement.prototype.$$ = function(selector) { + return window.$$(selector, this) +} +DocumentFragment.prototype.$$ = function(selector) { + return window.$$(selector, this) +} /* CONSOLE */ @@ -163,6 +171,7 @@ Object.defineProperty(Array.prototype, 'last', { window.quill = { rendering: [], + lastState: null, render: (el) => { if(el instanceof Shadow) { @@ -193,19 +202,6 @@ window.quill = { quill.rendering.pop() }, - loadPage: () => { - let URL = window.location.pathname - if(!window.routes[URL]) { - throw new Error("No URL for this route: ", URL) - } - - let pageClass = window[routes[URL]] - document.title = pageClass.title ?? document.title - document.body.innerHTML = "" - let page = new pageClass() - quill.render(page) - }, - isStack: (el) => { return el.classList.contains("HStack") || el.classList.contains("ZStack") || el.classList.contains("VStack") }, @@ -216,6 +212,7 @@ window.Shadow = class extends HTMLElement { super() } } + window.register = (el, tagname) => { if (typeof el.prototype.render !== 'function') { throw new Error("Element must have a render: " + el.prototype.constructor.name) @@ -230,6 +227,42 @@ window.register = (el, tagname) => { window[el.prototype.constructor.name] = function (...params) { let instance = new el(...params) + if(instance.state) { + let proxy = new Proxy(instance.state, { + get(target, prop, receiver) { + if (typeof prop === "symbol") { // Ignore internal / symbol accesses + return Reflect.get(target, prop, receiver); + } + + quill.lastLastState = quill.lastState + quill.lastState = prop; + return Reflect.get(target, prop, receiver); + }, + set(target, prop, value, receiver) { + const oldValue = target[prop]; + if (oldValue === value) return true; + + const result = Reflect.set(target, prop, value, receiver); + instance.stateWatchers[prop].forEach((cb) => cb()) + return result; + } + }); + Object.defineProperty(instance, "state", { + value: proxy, + writable: false, + configurable: false, + enumerable: true + }); + + let stateWatchers = {} + Object.keys(instance.state).forEach((key) => stateWatchers[key] = []) + Object.defineProperty(instance, "stateWatchers", { + value: stateWatchers, + writable: false, + configurable: false, + enumerable: true + }); + } quill.render(instance) return instance } @@ -314,6 +347,7 @@ function extendHTMLElementWithStyleSetters() { case "marginRight": case "textUnderlineOffset": + case "letterSpacing": return "unit-number" @@ -333,24 +367,27 @@ function extendHTMLElementWithStyleSetters() { switch (type) { case "unit-number": - HTMLElement.prototype[prop] = function(value, unit = "px") { - if ((typeof value !== "number" || isNaN(value)) && value !== "auto") { - throw new Error(`Invalid value for ${prop}: ${value}. Expected a number.`); - } + HTMLElement.prototype[prop] = StyleFunction(function(value, unit = "px") { if(value === "auto") { this.style[prop] = value return this } this.style[prop] = value + unit; + if (value !== "" && this.style[prop] === "") { + throw new Error(`Invalid CSS value for ${prop}: ` + value + unit); + } return this; - }; + }); break; case "string": - HTMLElement.prototype[prop] = function(value) { + HTMLElement.prototype[prop] = StyleFunction(function(value) { this.style[prop] = value; + if (value !== "" && this.style[prop] === "") { + throw new Error(`Invalid CSS value for ${prop}: ` + value); + } return this; - }; + }); break; } }); @@ -358,46 +395,86 @@ function extendHTMLElementWithStyleSetters() { extendHTMLElementWithStyleSetters(); +HTMLElement.prototype.addStateWatcher = function(field, cb) { + let parent = this + while(!(parent instanceof Shadow)) { + parent = parent.parentNode + } + parent.stateWatchers[field].push(cb) +} + +// Currently only works for one state variable in the function +// Could probably be fixed by just making lastState an array and clearing it out every function call? +HTMLElement.prototype.setUpState = function(styleFunc, cb) { + let format = (value) => {return Array.isArray(value) ? value : [value]} + + // 1. Run the callback to get the style argument and also update lastState + let styleArgs = format(cb()) + + // 2. Check if lastState has really been updated. If not, the user-provided cb did not access valid state + if(!quill.lastState) { + throw new Error("Quill: style state function does not access valid state") + } + + // 3. Construct function to run when state changes + let onStateChange = () => { + styleFunc.call(this, ...format(cb())) + } + + // 4. Now listen for the state to change + this.addStateWatcher(quill.lastState, onStateChange) + + // 5. Run the original function again, this time with the actual arguments + quill.lastState = null + styleFunc.call(this, ...styleArgs) +} + +function StyleFunction(func) { + let styleFunction = function(value, unit = "px") { + if(typeof value === 'function') { + this.setUpState(styleFunction, value) + return this + } else { + func.call(this, value, unit); // ".call" ensures that "this" is correct + return this + } + } + + return styleFunction +} + HTMLElement.prototype.styles = function(cb) { cb.call(this, this) return this } -HTMLElement.prototype.paddingVertical = function(value, unit = "px") { - if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value)) - throw new Error(`Invalid value: ${value}. Expected a number.`); +/* Type 1 */ + +HTMLElement.prototype.paddingVertical = StyleFunction(function(value, unit = "px") { this.style.paddingTop = value + unit this.style.paddingBottom = value + unit return this -} +}) -HTMLElement.prototype.paddingHorizontal = function(value, unit = "px") { - if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value)) - throw new Error(`Invalid value: ${value}. Expected a number.`); +HTMLElement.prototype.paddingHorizontal = StyleFunction(function(value, unit = "px") { this.style.paddingRight = value + unit this.style.paddingLeft = value + unit return this -} +}) -HTMLElement.prototype.marginVertical = function(value, unit = "px") { - if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value)) - throw new Error(`Invalid value: ${value}. Expected a number.`); +HTMLElement.prototype.marginVertical = StyleFunction(function(value, unit = "px") { this.style.marginTop = value + unit this.style.marginBottom = value + unit return this -} +}) -HTMLElement.prototype.marginHorizontal = function(value, unit = "px") { - if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value)) - throw new Error(`Invalid value: ${value}. Expected a number.`); +HTMLElement.prototype.marginHorizontal = StyleFunction(function(value, unit = "px") { this.style.marginRight = value + unit this.style.marginLeft = value + unit return this -} +}) -HTMLElement.prototype.fontSize = function(value, unit = "px") { - if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value)) - throw new Error(`Invalid value: ${value}. Expected a number.`); +HTMLElement.prototype.fontSize = StyleFunction(function(value, unit = "px") { switch(value) { case "6xl": @@ -441,7 +518,7 @@ HTMLElement.prototype.fontSize = function(value, unit = "px") { } this.style.fontSize = value + unit return this -} +}) function checkPositionType(el) { let computed = window.getComputedStyle(el).position @@ -450,6 +527,26 @@ function checkPositionType(el) { } } +HTMLElement.prototype.width = function(value, unit = "px") { + if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value)) + throw new Error(`Invalid value: ${value}. Expected a number.`); + this.style.width = value + unit + if(window.getComputedStyle(this).display === "inline") { + this.style.display = "block" + } + return this +} + +HTMLElement.prototype.height = function(value, unit = "px") { + if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value)) + throw new Error(`Invalid value: ${value}. Expected a number.`); + this.style.height = value + unit + if(window.getComputedStyle(this).display === "inline") { + this.style.display = "block" + } + return this +} + HTMLElement.prototype.x = function(value, unit = "px") { if (typeof value !== 'number' || isNaN(value)) throw new Error(`Invalid value: ${value}. Expected a number.`); @@ -661,7 +758,7 @@ window.form = function(cb) { return el } -window.input = function(placeholder, width, height) { +window.input = function(placeholder = "", width, height) { let el = document.createElement("input") el.placeholder = placeholder el.style.width = width @@ -670,14 +767,18 @@ window.input = function(placeholder, width, height) { return el } -window.label = function(text) { +window.label = function(inside) { let el = document.createElement("label") - el.innerText = text + if(typeof inside === "string") { + el.innerText = inside + } else { + el.render = inside + } quill.render(el) return el } -window.textarea = function(placeholder) { +window.textarea = function(placeholder = "") { let el = document.createElement("textarea") el.placeholder = placeholder quill.render(el) @@ -845,8 +946,8 @@ HTMLElement.prototype.onAppear = function(func) { }; HTMLElement.prototype.onClick = function(func) { - const onMouseDown = () => func.call(this, false); - const onMouseUp = () => func.call(this, true); + const onMouseDown = (e) => func.call(this, false, e); + const onMouseUp = (e) => func.call(this, true, e); this._storeListener("mousedown", onMouseDown); this._storeListener("mouseup", onMouseUp); return this; @@ -929,34 +1030,38 @@ HTMLElement.prototype.onTap = function(cb) { }; /* WHY THIS LISTENER IS THE WAY IT IS: -- If we dispatch the "navigate" event on the window (as one would expect for a "navigate" event), a listener placed on the element will not pick it up. -- However, if we add the listener on the window, it won't have the "this" scope that a callback normally would. Which makes it much less useful. -- Then, if we try to add that scope using bind(), it makes the function.toString() unreadable, which means we cannot detect duplicate listeners. -- Therefore, we just have to attach the navigate event to the element, and manually trigger that when the window listener fires. +- We can't just put a listener on the element, because a window "navigate" event won't trigger it +- We can't just put a listener on the window, because the "this" variable will only refer to the window +- And, if we try to re-add that scope using bind(), it makes the return value of .toString() unreadable, which means we cannot detect duplicate listeners. +- Therefore, we attach a global navigate event to the window, and each navigate event in this array, and manually trigger each event when the global one fires. */ navigateListeners = [] -HTMLElement.prototype.onNavigate = function(cb) { - this._storeListener("navigate", cb); - - let found = false - for(entry of navigateListeners) { - if(entry.cb.toString() === cb.toString() && - this.nodeName === entry.el.nodeName) { - found = true - break; - } - } - if(found === false) { - navigateListeners.push({el: this, cb: cb}) - } - - return this; -}; window.addEventListener("navigate", () => { for(entry of navigateListeners) { entry.el.dispatchEvent(new CustomEvent("navigate")) } }) +HTMLElement.prototype.onNavigate = function(cb) { + this._storeListener("navigate", cb); + + let found = false + let elementIndex = Array.from(this.parentNode.children).indexOf(this) + for(entry of navigateListeners) { + if( + entry.cb.toString() === cb.toString() + && entry.index === elementIndex + && this.nodeName === entry.el.nodeName + ) { + found = true + break; + } + } + if(found === false) { + navigateListeners.push({el: this, cb: cb, index: elementIndex}) + } + + return this; +}; /* Same principle applies diff --git a/ui/_/code/shared.css b/ui/_/code/shared.css index 4c1c4ca..fa0b3a9 100644 --- a/ui/_/code/shared.css +++ b/ui/_/code/shared.css @@ -43,12 +43,19 @@ } body { + margin: 0px; font-family: 'Bona Nova', sans-serif; font-size: 16px; background-color: var(--main); color: var(--accent); } +@media (max-width: 480px) { + body, html{ + overflow-x: hidden; + } +} + #title { padding: 5px 10px; font-size: 1.7rem; diff --git a/ui/mobile/ws/Connection.js b/ui/_/code/ws/Connection.js similarity index 91% rename from ui/mobile/ws/Connection.js rename to ui/_/code/ws/Connection.js index 69232ff..7d1d52c 100644 --- a/ui/mobile/ws/Connection.js +++ b/ui/_/code/ws/Connection.js @@ -10,8 +10,8 @@ class Connection { } init() { - if(window.location.hostname === "localhost") { - this.ws = new WebSocket("ws://" + "localhost:3003") + if(window.location.hostname.includes("local")) { + this.ws = new WebSocket("ws://" + window.location.host) } else { this.ws = new WebSocket("wss://" + window.location.hostname + window.location.pathname) } diff --git a/ui/mobile/ws/Socket.js b/ui/_/code/ws/Socket.js similarity index 100% rename from ui/mobile/ws/Socket.js rename to ui/_/code/ws/Socket.js diff --git a/ui/_/icons/jobs.svg b/ui/_/icons/jobs.svg new file mode 100644 index 0000000..f4a811f --- /dev/null +++ b/ui/_/icons/jobs.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/ui/_/icons/letter.svg b/ui/_/icons/letter.svg new file mode 100644 index 0000000..9a1d4d8 --- /dev/null +++ b/ui/_/icons/letter.svg @@ -0,0 +1,4 @@ + + + + diff --git a/ui/desktop/index.html b/ui/desktop/index.html index ca92388..24bd703 100644 --- a/ui/desktop/index.html +++ b/ui/desktop/index.html @@ -9,6 +9,6 @@ - + \ No newline at end of file diff --git a/ui/desktop/ws/Connection.js b/ui/desktop/ws/Connection.js index 69232ff..7d1d52c 100644 --- a/ui/desktop/ws/Connection.js +++ b/ui/desktop/ws/Connection.js @@ -10,8 +10,8 @@ class Connection { } init() { - if(window.location.hostname === "localhost") { - this.ws = new WebSocket("ws://" + "localhost:3003") + if(window.location.hostname.includes("local")) { + this.ws = new WebSocket("ws://" + window.location.host) } else { this.ws = new WebSocket("wss://" + window.location.hostname + window.location.pathname) } diff --git a/ui/mobile/apps/Forum/Forum.js b/ui/mobile/apps/Forum/Forum.js index 0a26881..8ee2f0b 100644 --- a/ui/mobile/apps/Forum/Forum.js +++ b/ui/mobile/apps/Forum/Forum.js @@ -30,70 +30,32 @@ class Forum extends Shadow { render() { ZStack(() => { - HStack(() => { - VStack(() => { - img("/_/icons/logo.svg", "2em") - .padding(0.8, em) - .borderRadius(12, px) - .marginHorizontal(1, em) - .onHover(function (hovering) { - if(hovering) { - this.style.background = "var(--darkbrown)" - } else { - this.style.background = "" - } - }) - .opacity(0) + VStack(() => { - img("/_/icons/place/austin.svg", "2em") - .padding(0.8, em) - .borderRadius(12, px) - .marginHorizontal(1, em) - .onHover(function (hovering) { - if(hovering) { - this.style.background = "var(--darkbrown)" - } else { - this.style.background = "" - } - }) - .opacity(0) + ForumPanel() - }) - .height(100, vh) - .paddingLeft(2, em) - .paddingRight(2, em) - .gap(1, em) - .marginTop(20, vh) - - VStack(() => { - - ForumPanel() - - input("Message Hyperia", "98%") - .paddingVertical(1, em) - .paddingLeft(2, pct) - .color("var(--accent)") - .background("var(--darkbrown)") - .marginBottom(6, em) - .border("none") - .fontSize(1, em) - .onKeyDown(function (e) { - if (e.key === "Enter") { - window.Socket.send({app: "FORUM", operation: "SEND", msg: {forum: "HY", text: this.value }}) - this.value = "" - } - }) - }) - .gap(0.5, em) - .width(100, pct) - .height(100, vh) - .alignHorizontal("center") - .alignVertical("end") + input("Message Hyperia", "98%") + .paddingVertical(1, em) + .paddingLeft(2, pct) + .color("var(--accent)") + .background("var(--darkbrown)") + .marginBottom(6, em) + .border("none") + .fontSize(1, em) + .onKeyDown(function (e) { + if (e.key === "Enter") { + window.Socket.send({app: "FORUM", operation: "SEND", msg: {forum: "HY", text: this.value }}) + this.value = "" + } + }) }) - .width(100, "%") - .height(87, vh) - .x(0).y(0, vh) + .gap(0.5, em) + .width(100, pct) + .height(100, vh) + .alignHorizontal("center") + .alignVertical("end") }) + .onAppear(() => document.body.style.backgroundColor = "var(--darkbrown)") .width(100, pct) .height(100, pct) } diff --git a/ui/mobile/apps/Forum/ForumPanel.js b/ui/mobile/apps/Forum/ForumPanel.js index d2a6fa4..9dfdde6 100644 --- a/ui/mobile/apps/Forum/ForumPanel.js +++ b/ui/mobile/apps/Forum/ForumPanel.js @@ -54,14 +54,12 @@ class ForumPanel extends Shadow { .paddingLeft(4, pct) .backgroundColor("var(--darkbrown)") .onAppear(async () => { - console.log("appear") requestAnimationFrame(() => { this.scrollTop = this.scrollHeight }); let res = await Socket.send({app: "FORUM", operation: "GET", msg: {forum: "HY", number: 100}}) if(!res) console.error("failed to get messages") if(res.msg.length > 0 && this.messages.length === 0) { - console.log("rerendering", res.msg) this.messages = res.msg this.rerender() } diff --git a/ui/mobile/components/AppMenu.js b/ui/mobile/components/AppMenu.js index 7b28205..d533876 100644 --- a/ui/mobile/components/AppMenu.js +++ b/ui/mobile/components/AppMenu.js @@ -1,19 +1,74 @@ class AppMenu extends Shadow { + selected = "" + + onNewSelection() { + this.$$("img").forEach((image) => { + image.style.background = "" + }) + } + render() { + console.log("rendering") HStack(() => { - img("/_/icons/mail.png", "2em", "2em") - img("/_/icons/Column.svg", "2em", "2em") - p("S") - p("S") - p("S") + img("/_/icons/Column.svg", "1.5em", "1.5em") + .attr({app: "forum"}) + .padding(0.5, em) + .borderRadius(10, px) + .onAppear(function () { + this.style.border = "1px solid black" + }) + .onClick((finished, e) => { + if(finished) { + this.onNewSelection() + } + e.target.style.background = "var(--accent)" + console.log(e.target, e.target.style.background) + if(finished) { + window.navigateTo("/") + } + }) + img("/_/icons/letter.svg", "1.5em", "1.5em") + .attr({app: "messages"}) + .padding(0.5, em) + .borderRadius(10, px) + .onAppear(function () { + this.style.border = "1px solid black" + }) + .onClick((finished, e) => { + if(finished) { + this.onNewSelection() + } + e.target.style.background = "rgb(112 150 114)" + if(finished) { + window.navigateTo("/messages") + } + }) + img("/_/icons/jobs.svg", "1.5em", "1.5em") + .attr({app: "jobs"}) + .padding(0.5, em) + .borderRadius(10, px) + .onAppear(function () { + this.style.border = "1px solid black" + }) + .onClick((finished, e) => { + if(finished) { + this.onNewSelection() + } + e.target.style.background = "#9392bb" + if(finished) { + window.navigateTo("/jobs") + } + }) }) .borderTop("1px solid black") - .height(3, em) + .height("auto") + .position('fixed') + .background("var(--main)") + .zIndex(1) .x(0).yBottom(0) .justifyContent("space-between") - .paddingHorizontal(2, em) + .paddingHorizontal(4, em) .paddingVertical(1, em) - .transform("translateY(-50%)") .width(100, vw) .boxSizing("border-box") } diff --git a/ui/mobile/components/Home.js b/ui/mobile/components/Home.js index 0e97d8e..09011c2 100644 --- a/ui/mobile/components/Home.js +++ b/ui/mobile/components/Home.js @@ -1,11 +1,37 @@ import "./AppMenu.js" +import "../apps/Forum/Forum.js" +import "../apps/Messages/Messages.js" +import "../apps/Jobs/Jobs.js" class Home extends Shadow { render() { ZStack(() => { + + ZStack(() => { + console.log("it's happening", window.location.pathname) + switch(window.location.pathname) { + case "/": + Forum() + break; + + case "/messages": + Messages() + break; + + case "/jobs": + Jobs() + break; + } + }) + .onNavigate(function () { + console.log("navigate") + this.rerender() + }) + AppMenu() }) + .overflowX("hidden") } } diff --git a/ui/mobile/components/LoadingCircle.js b/ui/mobile/components/LoadingCircle.js new file mode 100644 index 0000000..f71a86c --- /dev/null +++ b/ui/mobile/components/LoadingCircle.js @@ -0,0 +1,25 @@ +class LoadingCircle extends Shadow { + render() { + div() + .borderRadius(100, pct) + .width(2, em).height(2, em) + .x(45, pct).y(50, pct) + .center() + .backgroundColor("var(--accent") + .transition("transform 1.75s ease-in-out") + .onAppear(function () { + let growing = true; + + setInterval(() => { + if (growing) { + this.style.transform = "scale(1.5)"; + } else { + this.style.transform = "scale(0.7)"; + } + growing = !growing; + }, 750); + }); + } +} + +register(LoadingCircle) \ No newline at end of file diff --git a/ui/mobile/index.html b/ui/mobile/index.html index ca92388..24bd703 100644 --- a/ui/mobile/index.html +++ b/ui/mobile/index.html @@ -9,6 +9,6 @@ - + \ No newline at end of file diff --git a/ui/mobile/index.js b/ui/mobile/index.js index d0acae8..574b8e8 100644 --- a/ui/mobile/index.js +++ b/ui/mobile/index.js @@ -1,4 +1,4 @@ -import Socket from "./ws/Socket.js" +import Socket from "/_/code/ws/Socket.js" import "./components/Home.js" import util from "./util.js" diff --git a/ui/public/pages/Home.js b/ui/public/pages/Home.js index faead38..6f3c0e2 100644 --- a/ui/public/pages/Home.js +++ b/ui/public/pages/Home.js @@ -30,20 +30,28 @@ class Home extends Shadow { .left(50, vw).top(isMobile() ? 50 : 53, vh) .center() - p("H   Y   P   E   R   I   A  ") - .x(50, vw).y(isMobile() ? 50 : 53, vh) - .textAlign("center") - .center() - .color("var(--gold)") - .fontSize(isMobile() ? 6 : 5, vw) - .maxWidth(isMobile() ? 0.8 : 100, em) + if(!isMobile()) { + p("H   Y   P   E   R   I   A  ") + .x(50, vw).y(53, vh) + .textAlign("center") + .center() + .color("var(--gold)") + .fontSize(5, vw) + .maxWidth(isMobile() ? 1000 : 100, em) + } else { + p("H   Y   P   E   R   I   A  ") + .x(46, vw).y(isMobile() ? 50 : 53, vh) + .textAlign("center") + .color("var(--gold)") + .fontSize(5, vw) + } if(!isMobile()) { let text = "A Classical Christian Network" p(isMobile() ? text : text.toUpperCase()) .x(50, vw).yBottom(isMobile() ? 1 : 3, vh) .center() - .letterSpacing(0.3, em) + .letterSpacing(0.1, em) .width(isMobile() ? 80 : 100, vw) .fontSize(isMobile() ? 0.8 : 1, em) .textAlign("center")