This commit is contained in:
metacryst
2026-04-28 20:05:00 -05:00
commit 0d6c7683ff
123 changed files with 20922 additions and 0 deletions

33
components/AddButton.js Normal file
View File

@@ -0,0 +1,33 @@
class AddButton extends Shadow {
render() {
p("+")
.fontWeight("bolder")
.paddingVertical(1, em)
.boxSizing("border-box")
.paddingHorizontal(1.25, em)
.background("var(--quillred)")
.color("var(--headertext)")
.marginBottom(1, em)
.border("1px solid var(--headertext)")
.borderRadius(100, px)
.onTap(() => {
this.handleAdd()
})
}
handleAdd() {
const app = document.documentElement.attr("app")
switch (app) {
case "jobs":
$("jobform-").toggle()
break;
case "calendar":
$("eventform-").toggle()
break;
default:
break;
}
}
}
register(AddButton)

8
components/AppTitle.js Normal file
View File

@@ -0,0 +1,8 @@
function AppTitle(text) {
return h1(text)
.fontFamily("Laandbrau")
.letterSpacing(2, px)
.fontSize(2.3, em)
}
window.AppTitle = AppTitle

36
components/Avatar.js Normal file
View File

@@ -0,0 +1,36 @@
class Avatar extends Shadow {
constructor(person, size) {
super()
this.person = person
this.size = size
}
render() {
const { person, size } = this
if (person.image_path) {
img(`${config.SERVER}${person.image_path}`, `${size}em`, `${size}em`)
.borderRadius(50, pct)
.objectFit("cover")
.flexShrink(0)
} else {
const initials = [person.first_name?.[0], person.last_name?.[0]].filter(Boolean).join("").toUpperCase()
VStack(() => {
p(initials)
.margin(0).fontSize(size * 0.35, em).fontWeight("700")
.color("white").lineHeight("1")
})
.width(size, em).height(size, em).borderRadius(50, pct)
.background(this.avatarColor(`${person.first_name} ${person.last_name}`))
.justifyContent("center").alignItems("center").flexShrink(0)
}
}
avatarColor(name) {
const colors = ["#3b82f6", "#ef4444", "#10b981", "#f59e0b", "#8b5cf6", "#ec4899", "#06b6d4", "#84cc16"]
let hash = 0
for (let i = 0; i < (name || "").length; i++) hash = name.charCodeAt(i) + ((hash << 5) - hash)
return colors[Math.abs(hash) % colors.length]
}
}
register(Avatar)

62
components/BackButton.js Normal file
View File

@@ -0,0 +1,62 @@
class BackButton extends Shadow {
constructor(withBackground = true, isDown = false, toggle) {
super()
this.withBackground = withBackground;
this.isDown = isDown;
this.toggle = toggle;
}
render() {
const toggle = this.toggle;
const btn = div("➩")
.display("flex")
.fontSize(2, em)
.paddingTop(0, rem)
.paddingHorizontal(0.5, rem)
.paddingBottom(0.9, rem)
.zIndex(3)
.color("var(--darkaccent)")
.transform(this.isDown ? "rotate(90deg)" : "rotate(180deg)")
.transition("scale .2s, color .2s")
.onTouch(function (start, e) {
if (start) {
this.scale("1.5")
this.style.color = "var(--quillred)"
} else {
this.scale("")
this.style.color = "var(--darkaccent)"
e.stopPropagation();
toggle();
}
})
if (this.withBackground) {
btn
.width(3, rem)
.height(3, rem)
.padding(0)
.borderRadius(50, pct)
.color("var(--divider)")
.background("var(--darkaccent)")
.color()
.border("1.5px solid var(--divider)")
.alignItems("center")
.justifyContent("center")
.onTouch(function (start, e) {
if (start) {
this.scale("1.5")
this.color("var(--darkaccent)")
this.backgroundColor("var(--divider)")
} else {
this.color("var(--divider)")
this.backgroundColor("var(--darkaccent)")
this.scale("")
e.stopPropagation();
toggle();
}
})
}
}
}
register(BackButton)

193
components/BottomSheet.js Normal file
View File

@@ -0,0 +1,193 @@
css(`
bottomsheet- {
scrollbar-width: none;
-ms-overflow-style: none;
}
bottomsheet- .VStack::-webkit-scrollbar {
display: none;
width: 0px;
height: 0px;
}
bottomsheet- .VStack::-webkit-scrollbar-thumb {
background: transparent;
}
bottomsheet- .VStack::-webkit-scrollbar-track {
background: transparent;
}
`)
class BottomSheet extends Shadow {
swipeDragStartX = null;
swipeDragStartY = null;
swipeDragStartTime = null;
isSwiping = false;
swipeAxisLocked = false;
swipeIsVertical = false;
swipeTranslate = 0;
SWIPE_COMMIT_DISTANCE = window.outerHeight * 0.25;
SWIPE_VELOCITY_THRESHOLD = 0.4;
renderContent = null;
_closeOverride = null;
constructor(zOffset = 0) {
super()
this.zOffset = zOffset;
}
show(renderFn, onClose = null) {
this._closeOverride = null;
this.renderContent = renderFn;
this._onClose = onClose;
this.rerender();
requestAnimationFrame(() => requestAnimationFrame(() => this.setSheet(true)));
}
replace(renderFn) {
this.renderContent = renderFn;
this.rerender();
if (this.sheetEl) {
this.sheetEl.style.transition = "none";
this.sheetEl.style.top = "7.5%";
this.sheetEl.style.pointerEvents = "auto";
}
if (this.overlayEl) {
this.overlayEl.style.transition = "none";
this.overlayEl.style.background = "rgba(0, 0, 0, 0.3)";
this.overlayEl.style.pointerEvents = "auto";
}
}
render() {
ZStack(() => {
this.overlayEl = ZStack()
.position("fixed")
.inset(0)
.zIndex(90 + this.zOffset)
.background("transparent")
.pointerEvents("none")
.onTap(() => this.toggle())
this.sheetEl = ZStack(() => {
VStack(() => {
if (this.renderContent) this.renderContent()
})
.height(100, pct)
.position("relative")
})
.position("fixed")
.height(92.5, pct)
.top(100, pct)
.left(0).right(0)
.zIndex(100 + this.zOffset)
.background("var(--main)")
.borderTopLeftRadius("10px").borderTopRightRadius("10px")
.boxSizing("border-box")
.transition("top .3s")
.pointerEvents("none")
.onTouch((start, e) => this.handleSwipeTouch(start, e))
})
}
get isOpen() {
return this.sheetEl?.style.top === "7.5%";
}
toggle() {
this.setSheet(!this.isOpen);
}
forceClose() {
this._closeOverride = null
this.setSheet(false)
}
setSheet(open) {
if (!open && this._closeOverride) { this._closeOverride(); return }
if (this.overlayEl) {
this.overlayEl.style.transition = "background 0.3s";
this.overlayEl.style.pointerEvents = open ? "auto" : "none";
this.overlayEl.style.background = open ? "rgba(0, 0, 0, 0.3)" : "transparent";
}
if (this.sheetEl) {
this.sheetEl.style.transition = "top 0.3s";
this.sheetEl.style.top = open ? "7.5%" : "100%";
this.sheetEl.style.pointerEvents = open ? "auto" : "none";
}
if (!open) {
if (this._onClose) {
const cb = this._onClose;
this._onClose = null;
setTimeout(cb, 300);
}
this.isSwiping = false;
this.swipeTranslate = 0;
this.swipeDragStartX = null;
this.swipeDragStartY = null;
document.removeEventListener("touchmove", this.onSwipeMove);
}
}
handleSwipeTouch(start, e) {
if (start) {
if (this.sheetEl?.style.top !== "7.5%") return;
e.stopPropagation();
this.swipeDragStartX = e.touches[0].clientX;
this.swipeDragStartY = e.touches[0].clientY;
this.swipeDragStartTime = Date.now();
this.isSwiping = true;
this.swipeAxisLocked = false;
this.swipeIsVertical = false;
document.addEventListener("touchmove", this.onSwipeMove, { passive: false });
} else {
if (!this.isSwiping) return;
document.removeEventListener("touchmove", this.onSwipeMove);
if (!this.swipeIsVertical) {
this.isSwiping = false;
this.setSheet(true);
return;
}
const delta = e.changedTouches[0].clientY - this.swipeDragStartY;
const velocity = Math.abs(delta) / (Date.now() - this.swipeDragStartTime);
const shouldCommit = delta > 0 && (delta > this.SWIPE_COMMIT_DISTANCE || velocity > this.SWIPE_VELOCITY_THRESHOLD);
this.swipeTranslate = 0;
this.setSheet(!shouldCommit);
this.isSwiping = false;
this.swipeDragStartX = null;
this.swipeDragStartY = null;
}
}
onSwipeMove = (e) => {
if (!this.isSwiping) return;
const dx = e.touches[0].clientX - this.swipeDragStartX;
const dy = e.touches[0].clientY - this.swipeDragStartY;
if (!this.swipeAxisLocked) {
if (Math.abs(dx) < 5 && Math.abs(dy) < 5) return;
this.swipeAxisLocked = true;
this.swipeIsVertical = Math.abs(dy) > Math.abs(dx);
}
if (!this.swipeIsVertical) return;
const delta = e.touches[0].clientY - this.swipeDragStartY;
const scrollEl = this.querySelector(".VStack");
if (delta <= 0 || (scrollEl && scrollEl.scrollTop > 0)) return;
e.preventDefault();
this.swipeTranslate = delta;
this.sheetEl.style.transition = "";
this.sheetEl.style.top = `calc(7.5% + ${delta}px)`;
}
}
register(BottomSheet)

72
components/SearchBar.js Normal file
View File

@@ -0,0 +1,72 @@
css(`
searchbar- input::placeholder {
color: #5C504D;
}
`)
class SearchBar extends Shadow {
searchText
width
constructor(searchText, width) {
super()
this.searchText = searchText
this.width = width
}
render() {
form(() => {
input("Search", this.width)
.attr({ name: "searchText", type: "text" })
.attr({ value: this.searchText ? this.searchText : "" })
.paddingVertical(0.75, em)
.boxSizing("border-box")
.paddingHorizontal(1, em)
.background("var(--searchbackground)")
.color("gray")
.marginBottom(1, em)
.marginLeft(1, em)
.marginRight(0.5, em)
.border("1px solid color-mix(in srgb, var(--accent) 60%, transparent)")
.borderRadius(100, px)
.fontFamily("Arial")
.fontSize(1, em)
.cursor("not-allowed")
.onTouch(function (start) {
if (start) {
this.style.backgroundColor = "var(--accent)"
} else {
this.style.backgroundColor = "var(--searchbackground)"
}
})
})
.onSubmit(async (e) => {
e.preventDefault();
const data = new FormData(e.target);
this.dispatchSearchEvent(data.get("searchText"))
})
}
dispatchSearchEvent(searchText) {
const app = document.documentElement.attr("app")
switch (app) {
case "Jobs":
window.dispatchEvent(new CustomEvent('jobsearch', {
detail: { searchText: searchText }
}));
break;
case "Events":
window.dispatchEvent(new CustomEvent('eventsearch', {
detail: { searchText: searchText }
}));
break;
case "Announcements":
window.dispatchEvent(new CustomEvent('announcementsearch', {
detail: { searchText: searchText }
}));
break;
}
}
}
register(SearchBar)