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

View File

@@ -0,0 +1,414 @@
class PoliticsElections extends Shadow {
constructor(elections, levelFilter, onVote, onThumbsUp) {
super()
this.elections = elections
this.levelFilter = levelFilter
this.onVote = onVote
this.onThumbsUp = onThumbsUp
// which election card is "expanded" for voting
this.expandedId = null
}
get visible() {
let list = this.levelFilter === "all"
? this.elections
: this.elections.filter(e => e.level === this.levelFilter)
return [...list].sort((a, b) => new Date(a.date) - new Date(b.date))
}
get upcoming() { return this.visible.filter(e => new Date(e.date) >= new Date()) }
get past() { return this.visible.filter(e => new Date(e.date) < new Date()) }
render() {
VStack(() => {
// Header
VStack(() => {
h2("Elections")
.margin(0).fontSize(1.1, em).fontWeight("700").color("var(--headertext)")
p(`${this.upcoming.length} upcoming · ${this.past.length} recent`)
.margin(0).marginTop(0.2, em).fontSize(0.78, em)
.color("var(--headertext)").opacity(0.42)
})
.paddingHorizontal(1.75, em).paddingVertical(1.25, em)
.borderBottom("1px solid var(--divider)").flexShrink(0)
VStack(() => {
if (this.upcoming.length > 0) {
this.sectionLabel("UPCOMING")
this.upcoming.forEach(e => this.renderElectionCard(e, false))
}
if (this.past.length > 0) {
this.sectionLabel("RECENT")
this.past.forEach(e => this.renderElectionCard(e, true))
}
if (this.visible.length === 0) {
VStack(() => {
p("🗳")
.margin(0).fontSize(2.2, em).opacity(0.18)
p("No elections found for this level")
.margin(0).marginTop(0.6, em).fontSize(0.88, em)
.color("var(--headertext)").opacity(0.32)
})
.flex(1).justifyContent("center").alignItems("center").paddingTop(4, em)
}
})
.overflowY("auto").flex(1)
.paddingHorizontal(1.75, em).paddingBottom(2, em).paddingTop(0.5, em)
})
.height(100, pct).width(100, pct).overflow("hidden")
}
renderElectionCard(election, isPast) {
const isExpanded = this.expandedId === election.id
const hasVoted = !!election.userVoteId
const daysAway = Math.ceil((new Date(election.date) - new Date()) / 86400000)
VStack(() => {
// ── Card top row ──────────────────────────────────────────
HStack(() => {
// Level badge
VStack(() => {
p(this.levelIcon(election.level))
.margin(0).fontSize(0.8, em).lineHeight("1")
})
.width(2, em).height(2, em).borderRadius(0.38, em)
.background("var(--darkaccent)").border("1px solid var(--divider)")
.justifyContent("center").alignItems("center").flexShrink(0)
VStack(() => {
HStack(() => {
p(election.title)
.margin(0).fontSize(0.95, em).fontWeight("700")
.color("var(--headertext)").flex(1).minWidth(0)
// Date chip
HStack(() => {
if (!isPast && daysAway <= 30) {
p(`${daysAway}d away`)
.margin(0).fontSize(0.68, em).fontWeight("700")
.color(daysAway <= 7 ? "#ef4444" : "#f59e0b")
} else {
p(this.formatDate(election.date))
.margin(0).fontSize(0.72, em)
.color("var(--headertext)").opacity(0.4)
}
})
.paddingHorizontal(0.55, em).paddingVertical(0.18, em)
.background(
!isPast && daysAway <= 7 ? "rgba(239,68,68,0.08)" :
!isPast && daysAway <= 30 ? "rgba(245,158,11,0.08)" :
"var(--darkaccent)"
)
.border(`1px solid ${
!isPast && daysAway <= 7 ? "rgba(239,68,68,0.2)" :
!isPast && daysAway <= 30 ? "rgba(245,158,11,0.2)" :
"var(--divider)"
}`)
.borderRadius(100, px).flexShrink(0)
})
.alignItems("center").gap(0.5, em)
HStack(() => {
p(this.capitalize(election.type))
.margin(0).paddingHorizontal(0.45, em).paddingVertical(0.1, em)
.background("var(--darkaccent)").border("1px solid var(--divider)")
.borderRadius(100, px).fontSize(0.68, em)
.color("var(--headertext)").opacity(0.55)
p(this.capitalize(election.level))
.margin(0).paddingHorizontal(0.45, em).paddingVertical(0.1, em)
.background("var(--darkaccent)").border("1px solid var(--divider)")
.borderRadius(100, px).fontSize(0.68, em)
.color("var(--headertext)").opacity(0.55)
if (election.description) {
p(election.description)
.margin(0).fontSize(0.75, em)
.color("var(--headertext)").opacity(0.45)
}
})
.gap(0.35, em).alignItems("center").marginTop(0.38, em)
})
.flex(1).minWidth(0).gap(0)
})
.gap(0.75, em).alignItems("flex-start")
// ── Candidates ────────────────────────────────────────────
if (election.candidates.length > 0) {
VStack(() => {
// Candidate vs. row
if (election.candidates.length >= 2) {
this.renderCandidateMatchup(election, isPast)
} else {
// Single candidate / unopposed
this.renderSingleCandidate(election.candidates[0], election, isPast)
}
})
.marginTop(1, em)
}
// ── Footer: thumbs up + weigh in ──────────────────────────
HStack(() => {
// Thumbs up / follow this race
HStack(() => {
button(election.userThumbsUp ? "👍" : "👍")
.padding(0).border("none").background("transparent")
.cursor("pointer").fontSize(0.95, em)
.onClick((done) => {if(!done) return; this.onThumbsUp(election.id)})
p(election.thumbsUp > 0 ? `${election.thumbsUp}` : "Follow")
.margin(0).fontSize(0.75, em)
.color("var(--headertext)")
.opacity(election.userThumbsUp ? 1 : 0.42)
.fontWeight(election.userThumbsUp ? "600" : "400")
})
.gap(0.3, em).alignItems("center")
.paddingHorizontal(0.65, em).paddingVertical(0.32, em)
.background(election.userThumbsUp ? "rgba(59,130,246,0.08)" : "transparent")
.border(`1px solid ${election.userThumbsUp ? "rgba(59,130,246,0.25)" : "var(--divider)"}`)
.borderRadius(100, px).cursor("pointer")
.onClick((done) => {if(!done) return; this.onThumbsUp(election.id)})
HStack(() => {}).flex(1)
if (!isPast && !hasVoted) {
button(isExpanded ? "Close" : "Weigh In →")
.paddingHorizontal(1, em).paddingVertical(0.38, em)
.background(isExpanded ? "transparent" : "var(--quillred)")
.border(isExpanded ? "1px solid var(--divider)" : "none")
.borderRadius(0.45, em)
.color(isExpanded ? "var(--headertext)" : "white")
.fontSize(0.82, em).fontWeight("600").cursor("pointer")
.opacity(isExpanded ? 0.6 : 1)
.onClick((done) => {if(!done) return;
this.expandedId = isExpanded ? null : election.id
this.rerender()
})
} else if (hasVoted) {
HStack(() => {
p("✓ You weighed in")
.margin(0).fontSize(0.75, em).fontWeight("600").color("#22c55e")
})
.paddingHorizontal(0.75, em).paddingVertical(0.32, em)
.background("rgba(34,197,94,0.08)").border("1px solid rgba(34,197,94,0.2)")
.borderRadius(100, px)
}
})
.alignItems("center").marginTop(0.9, em)
// ── Expanded vote panel ───────────────────────────────────
if (isExpanded && !hasVoted) {
VStack(() => {
p("Who do you support?")
.margin(0).marginBottom(0.75, em).fontSize(0.8, em).fontWeight("600")
.color("var(--headertext)").opacity(0.55)
.textAlign("center")
HStack(() => {
election.candidates.forEach((candidate, i) => {
const color = this.partyColor(candidate.party)
if (i > 0) {
p("vs")
.margin(0).paddingHorizontal(0.6, em)
.fontSize(0.72, em).fontWeight("700")
.color("var(--headertext)").opacity(0.3)
.alignSelf("center")
}
button(() => {
VStack(() => {
p(candidate.initials || candidate.name[0])
.margin(0).fontSize(0.9, em).fontWeight("800").color("white").lineHeight("1")
})
.width(2.2, em).height(2.2, em).borderRadius(50, pct)
.background(color).justifyContent("center").alignItems("center")
.marginBottom(0.5, em)
p(candidate.name)
.margin(0).fontSize(0.82, em).fontWeight("700")
.color("var(--headertext)").textAlign("center")
p(`${candidate.party}${candidate.incumbent ? " · Incumbent" : ""}`)
.margin(0).marginTop(0.15, em).fontSize(0.68, em)
.color(color).textAlign("center")
})
.flex(1)
.paddingVertical(0.9, em)
.paddingHorizontal(0.5, em)
.background(`${color}0d`)
.border(`2px solid ${color}33`)
.borderRadius(0.65, em)
.cursor("pointer")
.display("flex").flexDirection("column").alignItems("center")
.onClick((done) => {if(!done) return;
this.expandedId = null
this.onVote(election.id, candidate.id)
})
})
})
.gap(0.5, em).width(100, pct)
})
.marginTop(0.85, em)
.padding(1, em)
.background("var(--darkaccent)")
.border("1px solid var(--divider)")
.borderRadius(0.65, em)
.alignItems("center")
}
})
.padding(1.2, em)
.border("1px solid var(--divider)")
.borderRadius(0.75, em)
.marginBottom(0.85, em)
.boxSizing("border-box")
.opacity(isPast ? 0.72 : 1)
}
renderCandidateMatchup(election, isPast) {
const totalVotes = election.candidates.reduce((s, c) => s + (c.votes || 0), 0)
HStack(() => {
election.candidates.slice(0, 4).forEach((candidate, i) => {
const pct = totalVotes > 0 ? Math.round((candidate.votes / totalVotes) * 100) : null
const color = this.partyColor(candidate.party)
const isUserVote = election.userVoteId === candidate.id
if (i > 0) {
VStack(() => {
p("vs").margin(0).fontSize(0.68, em).fontWeight("700")
.color("var(--headertext)").opacity(0.28)
})
.justifyContent("center").paddingHorizontal(0.5, em).flexShrink(0)
}
VStack(() => {
// Party color bar top
VStack(() => {})
.height(3, px).width(100, pct).background(color).flexShrink(0)
VStack(() => {
// Name + incumbent
HStack(() => {
VStack(() => {
p(candidate.initials || candidate.name.split(" ").map(w => w[0]).join(""))
.margin(0).fontSize(0.72, em).fontWeight("800").color("white").lineHeight("1")
})
.width(1.9, em).height(1.9, em).borderRadius(50, pct)
.background(color).justifyContent("center").alignItems("center").flexShrink(0)
VStack(() => {
p(candidate.name)
.margin(0).fontSize(0.82, em).fontWeight("700").color("var(--headertext)")
.overflow("hidden").whiteSpace("nowrap").textOverflow("ellipsis")
HStack(() => {
p(candidate.party)
.margin(0).paddingHorizontal(0.35, em).paddingVertical(0.08, em)
.background(`${color}18`).border(`1px solid ${color}33`)
.borderRadius(100, px).fontSize(0.62, em).fontWeight("700").color(color)
if (candidate.incumbent) {
p("Incumbent")
.margin(0).paddingHorizontal(0.35, em).paddingVertical(0.08, em)
.background("var(--darkaccent)").border("1px solid var(--divider)")
.borderRadius(100, px).fontSize(0.62, em).color("var(--headertext)").opacity(0.45)
}
})
.gap(0.25, em).alignItems("center").marginTop(0.18, em)
})
.flex(1).minWidth(0)
})
.gap(0.55, em).alignItems("center")
// Support bar
if (pct !== null && totalVotes > 0) {
VStack(() => {
VStack(() => {
HStack(() => {
VStack(() => {})
.height(100, pct).width(pct, pct)
.background(color).borderRadius(100, px)
.transition("width 0.5s ease")
})
.height(100, pct).width(100, pct).alignItems("center")
})
.width(100, pct).height(6, px).background(`${color}22`).borderRadius(100, px).overflow("hidden")
HStack(() => {
p(`${pct}% community support`)
.margin(0).fontSize(0.68, em).color("var(--headertext)").opacity(0.42)
if (isUserVote) {
p("· Your pick ✓")
.margin(0).fontSize(0.68, em).color(color).fontWeight("600")
}
})
.gap(0.3, em).alignItems("center").marginTop(0.35, em)
})
.marginTop(0.65, em).width(100, pct)
}
})
.padding(0.75, em).flex(1)
})
.flex(1).minWidth(0)
.background("var(--darkaccent)")
.border(`1px solid ${isUserVote ? color + "55" : "var(--divider)"}`)
.borderRadius(0.55, em).overflow("hidden")
.boxSizing("border-box")
})
})
.gap(0).alignItems("stretch").width(100, pct)
}
renderSingleCandidate(candidate, election, isPast) {
const color = this.partyColor(candidate.party)
HStack(() => {
VStack(() => {
p(candidate.initials || candidate.name[0])
.margin(0).fontSize(0.85, em).fontWeight("800").color("white").lineHeight("1")
})
.width(2.4, em).height(2.4, em).borderRadius(50, pct)
.background(color).justifyContent("center").alignItems("center").flexShrink(0)
VStack(() => {
p(candidate.name)
.margin(0).fontSize(0.9, em).fontWeight("700").color("var(--headertext)")
HStack(() => {
p(candidate.party).margin(0).fontSize(0.7, em).fontWeight("600").color(color)
if (candidate.incumbent) {
p("· Incumbent").margin(0).fontSize(0.7, em).color("var(--headertext)").opacity(0.42)
}
p("· Unopposed").margin(0).fontSize(0.7, em).color("var(--headertext)").opacity(0.35)
})
.gap(0.35, em).alignItems("center").marginTop(0.2, em)
})
.flex(1)
})
.gap(0.75, em).alignItems("center")
.padding(0.85, em)
.background("var(--darkaccent)").border("1px solid var(--divider)").borderRadius(0.55, em)
}
sectionLabel(text) {
p(text)
.margin(0).marginTop(0.85, em).marginBottom(0.65, em)
.fontSize(0.62, em).fontWeight("700").letterSpacing("0.07em")
.color("var(--headertext)").opacity(0.35)
}
levelIcon(level) {
return { federal: "🇺🇸", state: "🏠", local: "📍" }[level] || "🗳"
}
formatDate(raw) {
return new Date(raw).toLocaleDateString([], { month: "long", day: "numeric", year: "numeric" })
}
capitalize(s) {
return s ? s[0].toUpperCase() + s.slice(1) : s
}
partyColor(party) {
return { R: "#ef4444", D: "#3b82f6", I: "#8b5cf6", L: "#f59e0b", G: "#22c55e" }[party] || "#6b7280"
}
}
register(PoliticsElections)

View File

@@ -0,0 +1,214 @@
class PoliticsRepresentatives extends Shadow {
constructor(reps, levelFilter) {
super()
this.reps = reps
this.levelFilter = levelFilter
}
get visible() {
if (this.levelFilter === "all") return this.reps
return this.reps.filter(r => r.level === this.levelFilter)
}
get grouped() {
const order = ["federal", "state", "local"]
const map = {}
order.forEach(l => { map[l] = [] })
this.visible.forEach(r => {
if (map[r.level]) map[r.level].push(r)
})
return order.map(l => ({ level: l, reps: map[l] })).filter(g => g.reps.length)
}
render() {
VStack(() => {
// Header
VStack(() => {
HStack(() => {
VStack(() => {
h2("Your Representatives")
.margin(0).fontSize(1.1, em).fontWeight("700").color("var(--headertext)")
p(`${this.visible.length} officials across your districts`)
.margin(0).marginTop(0.2, em).fontSize(0.78, em)
.color("var(--headertext)").opacity(0.42)
})
// Party breakdown pill
HStack(() => {
const counts = { R: 0, D: 0, I: 0 }
this.visible.forEach(r => { counts[r.party] = (counts[r.party] || 0) + 1 })
if (counts.R) this.partyCount(counts.R, "R")
if (counts.D) this.partyCount(counts.D, "D")
if (counts.I) this.partyCount(counts.I, "I")
})
.gap(0.5, em).alignItems("center")
})
.justifyContent("space-between").alignItems("flex-start")
})
.paddingHorizontal(1.75, em).paddingVertical(1.25, em)
.borderBottom("1px solid var(--divider)").flexShrink(0)
// Groups
VStack(() => {
this.grouped.forEach(group => {
VStack(() => {
// Level header
HStack(() => {
HStack(() => {})
.flex(1).height(1, px).background("var(--divider)")
p(this.levelLabel(group.level))
.margin(0).marginHorizontal(0.9, em)
.fontSize(0.68, em).fontWeight("700").letterSpacing("0.08em")
.color("var(--headertext)").opacity(0.35)
.whiteSpace("nowrap")
HStack(() => {})
.flex(1).height(1, px).background("var(--divider)")
})
.alignItems("center").marginBottom(1, em)
// Cards
HStack(() => {
group.reps.forEach(rep => this.renderCard(rep))
})
.gap(0.85, em).flexWrap("wrap")
})
.paddingTop(1.35, em).paddingBottom(0.5, em)
})
})
.paddingHorizontal(1.75, em)
.overflowY("auto").flex(1)
})
.height(100, pct).width(100, pct).overflow("hidden")
}
renderCard(rep) {
const partyColor = this.partyColor(rep.party)
const partyBg = this.partyBg(rep.party)
VStack(() => {
// Party accent bar
VStack(() => {})
.height(4, px).width(100, pct)
.background(partyColor).flexShrink(0)
VStack(() => {
// Avatar + status
ZStack(() => {
// Avatar circle
VStack(() => {
p(rep.initials)
.margin(0).fontSize(1.15, em).fontWeight("800")
.color("white").lineHeight("1")
})
.width(3.8, em).height(3.8, em).borderRadius(50, pct)
.background(partyColor)
.justifyContent("center").alignItems("center")
.flexShrink(0)
// Party badge bottom-right
VStack(() => {
p(rep.party)
.margin(0).fontSize(0.52, em).fontWeight("800")
.color("white").lineHeight("1")
})
.position("absolute").bottom(0).right(0)
.width(1.3, em).height(1.3, em).borderRadius(50, pct)
.background(partyColor)
.boxSizing("border-box")
.justifyContent("center").alignItems("center")
})
.position("relative")
.width(3.8, em).height(3.8, em).marginBottom(0.85, em)
// Name
p(rep.name)
.margin(0).fontSize(0.92, em).fontWeight("700")
.color("var(--headertext)").lineHeight("1.25").textAlign("center")
// Title
p(rep.title)
.margin(0).marginTop(0.22, em).fontSize(0.72, em)
.color("var(--headertext)").opacity(0.55)
.textAlign("center").lineHeight("1.35")
// District chip
if (rep.district) {
p(rep.district)
.margin(0).marginTop(0.55, em)
.paddingHorizontal(0.6, em).paddingVertical(0.18, em)
.background(partyBg).border(`1px solid ${partyColor}33`)
.borderRadius(100, px)
.fontSize(0.68, em).fontWeight("600")
.color(partyColor)
}
// Since
if (rep.since) {
p(`Since ${rep.since}`)
.margin(0).marginTop(0.42, em)
.fontSize(0.68, em).color("var(--headertext)").opacity(0.32)
}
// Contact row
HStack(() => {
if (rep.phone) {
this.contactBtn("📞", rep.phone)
}
if (rep.website) {
this.contactBtn("🌐", rep.website)
}
})
.gap(0.4, em).marginTop(0.75, em)
})
.padding(1.1, em).alignItems("center")
})
.width(200, px).minWidth(180, px)
.background("var(--main)")
.border("1px solid var(--divider)")
.borderRadius(0.65, em)
.overflow("hidden")
.boxSizing("border-box")
.flexShrink(0)
}
contactBtn(icon, value) {
button(icon)
.padding(0.35, em)
.background("var(--darkaccent)")
.border("1px solid var(--divider)")
.borderRadius(0.4, em)
.fontSize(0.8, em)
.cursor("pointer")
.attr({ title: value })
}
partyCount(count, party) {
HStack(() => {
VStack(() => {})
.width(0.5, em).height(0.5, em).borderRadius(50, pct)
.background(this.partyColor(party)).flexShrink(0)
p(`${count} ${party}`)
.margin(0).fontSize(0.75, em).fontWeight("600")
.color(this.partyColor(party))
})
.gap(0.3, em).alignItems("center")
.paddingHorizontal(0.6, em).paddingVertical(0.22, em)
.background(this.partyBg(party))
.border(`1px solid ${this.partyColor(party)}33`)
.borderRadius(100, px)
}
levelLabel(level) {
return { federal: "FEDERAL DELEGATION", state: "STATE DELEGATION", local: "LOCAL OFFICIALS" }[level] || level.toUpperCase()
}
partyColor(party) {
return { R: "#ef4444", D: "#3b82f6", I: "#8b5cf6", L: "#f59e0b", G: "#22c55e" }[party] || "#6b7280"
}
partyBg(party) {
return { R: "rgba(239,68,68,0.08)", D: "rgba(59,130,246,0.08)", I: "rgba(139,92,246,0.08)", L: "rgba(245,158,11,0.08)", G: "rgba(34,197,94,0.08)" }[party] || "var(--darkaccent)"
}
}
register(PoliticsRepresentatives)

View File

@@ -0,0 +1,163 @@
class PoliticsSidebar extends Shadow {
constructor(view, levelFilter, onViewChange, onLevelChange, jurisdiction, elections) {
super()
this.view = view
this.levelFilter = levelFilter
this.onViewChange = onViewChange
this.onLevelChange = onLevelChange
this.jurisdiction = jurisdiction
this.elections = elections
}
render() {
VStack(() => {
// ── App title ─────────────────────────────────────────────
VStack(() => {
p("⚖️")
.margin(0).fontSize(1.6, em).lineHeight("1")
h2("Civics")
.margin(0).marginTop(0.35, em).fontSize(1.05, em).fontWeight("700")
.color("var(--headertext)")
p("Your Political Dashboard")
.margin(0).marginTop(0.12, em).fontSize(0.72, em)
.color("var(--headertext)").opacity(0.38)
})
.paddingHorizontal(1.1, em).paddingTop(1.2, em).paddingBottom(0.9, em)
// ── Divider ───────────────────────────────────────────────
VStack(() => {}).height(1, px).background("var(--divider)").marginHorizontal(1, em)
// ── Main nav ──────────────────────────────────────────────
VStack(() => {
this.sectionLabel("VIEWS")
this.navItem("🏛", "Representatives", "representatives")
this.navItem("🗳", "Elections", "elections")
})
.paddingTop(0.75, em)
VStack(() => {}).height(1, px).background("var(--divider)").marginHorizontal(1, em).marginVertical(0.5, em)
// ── Level filter ──────────────────────────────────────────
VStack(() => {
this.sectionLabel("GOVERNMENT LEVEL")
this.levelItem("all", "🇺🇸 + 🏠 All")
this.levelItem("federal", "🇺🇸 Federal")
this.levelItem("state", "🏠 State")
this.levelItem("local", "📍 Local")
})
VStack(() => {}).height(1, px).background("var(--divider)").marginHorizontal(1, em).marginVertical(0.5, em)
// ── Jurisdiction card ─────────────────────────────────────
VStack(() => {
this.sectionLabel("YOUR DISTRICT")
VStack(() => {
this.jurisdictionRow("Federal", this.jurisdiction.federal)
this.jurisdictionRow("State Senate", this.jurisdiction.stateSenate)
this.jurisdictionRow("State House", this.jurisdiction.stateHouse)
this.jurisdictionRow("County", this.jurisdiction.county)
})
.padding(0.75, em)
.background("var(--darkaccent)")
.border("1px solid var(--divider)")
.borderRadius(0.55, em)
.marginHorizontal(0.75, em)
.gap(0)
})
.paddingTop(0)
VStack(() => {}).flex(1)
// ── Next election countdown ───────────────────────────────
VStack(() => {
const next = this.elections
.filter(e => new Date(e.date) > new Date())
.sort((a, b) => new Date(a.date) - new Date(b.date))[0]
if (next) {
const days = Math.ceil((new Date(next.date) - new Date()) / 86400000)
VStack(() => {
p("NEXT ELECTION")
.margin(0).fontSize(0.6, em).fontWeight("700").letterSpacing("0.07em")
.color("var(--headertext)").opacity(0.35)
p(next.title)
.margin(0).marginTop(0.35, em).fontSize(0.8, em).fontWeight("600")
.color("var(--headertext)").lineHeight("1.3")
HStack(() => {
p(`${days}`)
.margin(0).fontSize(1.6, em).fontWeight("800")
.color("var(--quillred)").lineHeight("1")
p("days away")
.margin(0).marginLeft(0.35, em).fontSize(0.72, em)
.color("var(--headertext)").opacity(0.45)
.alignSelf("flex-end").paddingBottom(0.1, em)
})
.alignItems("flex-end").marginTop(0.5, em)
})
.padding(0.85, em)
.background("var(--darkaccent)")
.border("1px solid var(--divider)")
.borderRadius(0.55, em)
.cursor("pointer")
.onClick((done) => {if(!done) return; this.onViewChange("elections")})
}
})
.paddingHorizontal(0.75, em).paddingBottom(1.1, em).flexShrink(0)
})
.height(100, pct).width(100, pct).boxSizing("border-box").overflowY("auto")
}
navItem(icon, label, viewId) {
const isActive = this.view === viewId
HStack(() => {
p(icon).margin(0).fontSize(0.95, em).lineHeight("1").flexShrink(0)
p(label).margin(0).fontSize(0.88, em).fontWeight(isActive ? "600" : "400")
.color("var(--headertext)").opacity(isActive ? 1 : 0.65)
})
.gap(0.62, em).paddingHorizontal(0.85, em).paddingVertical(0.45, em)
.marginHorizontal(0.4, em)
.borderRadius(0.45, em)
.background(isActive ? "var(--app)" : "transparent")
.cursor("pointer").alignItems("center")
.width("calc(100% - 0.8em)").boxSizing("border-box")
.onClick((done) => {if(!done) return; this.onViewChange(viewId)})
}
levelItem(level, label) {
const isActive = this.levelFilter === level
HStack(() => {
p(label).margin(0).fontSize(0.85, em).fontWeight(isActive ? "600" : "400")
.color("var(--headertext)").opacity(isActive ? 1 : 0.58)
})
.paddingHorizontal(0.85, em).paddingVertical(0.38, em)
.marginHorizontal(0.4, em)
.borderRadius(0.45, em)
.background(isActive ? "var(--app)" : "transparent")
.cursor("pointer")
.width("calc(100% - 0.8em)").boxSizing("border-box")
.onClick((done) => {if(!done) return; this.onLevelChange(level)})
}
jurisdictionRow(label, value) {
HStack(() => {
p(label)
.margin(0).fontSize(0.68, em).color("var(--headertext)").opacity(0.4)
.width(5.5, em).flexShrink(0)
p(value)
.margin(0).fontSize(0.72, em).fontWeight("600").color("var(--headertext)")
.flex(1).minWidth(0).overflow("hidden").whiteSpace("nowrap").textOverflow("ellipsis")
})
.paddingVertical(0.32, em)
.borderBottom("1px solid var(--divider)")
.alignItems("flex-start").gap(0.4, em)
}
sectionLabel(text) {
p(text)
.margin(0).marginBottom(0.28, em).paddingHorizontal(1.1, em)
.fontSize(0.62, em).fontWeight("700").letterSpacing("0.07em")
.color("var(--headertext)").opacity(0.35)
}
}
register(PoliticsSidebar)

View File

@@ -0,0 +1,266 @@
import "./PoliticsSidebar.js"
import "./PoliticsRepresentatives.js"
import "./PoliticsElections.js"
css(`
politics- {
font-family: 'Arial';
scrollbar-width: none;
-ms-overflow-style: none;
}
`)
class Politics extends Shadow {
constructor() {
super()
this.view = "representatives"
this.levelFilter = "all"
this.jurisdiction = {
federal: "TX-21",
stateSenate: "SD-25",
stateHouse: "HD-20",
county: "Comal County",
}
this.representatives = [
// ── Federal ───────────────────────────────────────────────
{
id: 1, level: "federal",
name: "John Cornyn", initials: "JC",
title: "U.S. Senator", party: "R",
district: "Texas (Class 2)", since: 2002,
phone: "(202) 224-2934", website: "cornyn.senate.gov",
},
{
id: 2, level: "federal",
name: "Ted Cruz", initials: "TC",
title: "U.S. Senator", party: "R",
district: "Texas (Class 1)", since: 2013,
phone: "(202) 224-5922", website: "cruz.senate.gov",
},
{
id: 3, level: "federal",
name: "Chip Roy", initials: "CR",
title: "U.S. Representative", party: "R",
district: "TX-21", since: 2019,
phone: "(202) 225-4236", website: "roy.house.gov",
},
// ── State ─────────────────────────────────────────────────
{
id: 4, level: "state",
name: "Donna Campbell", initials: "DC",
title: "Texas State Senator", party: "R",
district: "Senate District 25", since: 2013,
phone: "(512) 463-0125", website: "senate.texas.gov/member.php?d=25",
},
{
id: 5, level: "state",
name: "Terry Wilson", initials: "TW",
title: "Texas State Representative", party: "R",
district: "House District 20", since: 2017,
phone: "(512) 463-0309", website: "house.texas.gov/members/member-page/?district=20",
},
{
id: 6, level: "state",
name: "Ken Paxton", initials: "KP",
title: "Texas Attorney General", party: "R",
district: "Statewide", since: 2015,
phone: "(512) 463-2100", website: "texasattorneygeneral.gov",
},
// ── Local ─────────────────────────────────────────────────
{
id: 7, level: "local",
name: "Sherman Krause", initials: "SK",
title: "Comal County Judge", party: "R",
district: "Comal County", since: 2019,
phone: "(830) 221-1100", website: "co.comal.tx.us",
},
{
id: 8, level: "local",
name: "Mark Reynolds", initials: "MR",
title: "County Sheriff", party: "R",
district: "Comal County", since: 2017,
phone: "(830) 620-3400", website: null,
},
{
id: 9, level: "local",
name: "Jane Guerrero", initials: "JG",
title: "New Braunfels Mayor", party: "I",
district: "City of New Braunfels", since: 2021,
phone: "(830) 221-4000", website: "newbraunfels.gov",
},
]
const future = (d) => new Date(Date.now() + d * 86400000)
const past = (d) => new Date(Date.now() - d * 86400000)
this.elections = [
// Upcoming
{
id: 1,
level: "federal",
title: "U.S. Senate — Texas",
type: "general",
date: future(209),
description: "Class 2 seat — John Cornyn defending",
userVoteId: null,
userThumbsUp: false,
thumbsUp: 14,
candidates: [
{ id: 101, name: "John Cornyn", initials: "JC", party: "R", incumbent: true, votes: 312 },
{ id: 102, name: "Colin Allred", initials: "CA", party: "D", incumbent: false, votes: 187 },
]
},
{
id: 2,
level: "federal",
title: "U.S. House — TX-21",
type: "general",
date: future(209),
description: "Chip Roy vs. challenger",
userVoteId: null,
userThumbsUp: true,
thumbsUp: 31,
candidates: [
{ id: 201, name: "Chip Roy", initials: "CR", party: "R", incumbent: true, votes: 278 },
{ id: 202, name: "Marc Segers", initials: "MS", party: "D", incumbent: false, votes: 91 },
]
},
{
id: 3,
level: "state",
title: "Texas House District 20",
type: "primary",
date: future(42),
description: "Republican primary — open seat",
userVoteId: null,
userThumbsUp: false,
thumbsUp: 8,
candidates: [
{ id: 301, name: "Terry Wilson", initials: "TW", party: "R", incumbent: true, votes: 203 },
{ id: 302, name: "Brad Fischer", initials: "BF", party: "R", incumbent: false, votes: 144 },
]
},
{
id: 4,
level: "local",
title: "Comal County Commissioner Pct. 2",
type: "general",
date: future(12),
description: "Precinct 2 seat",
userVoteId: null,
userThumbsUp: false,
thumbsUp: 5,
candidates: [
{ id: 401, name: "Lisa Hartmann", initials: "LH", party: "R", incumbent: false, votes: 89 },
{ id: 402, name: "David Moreno", initials: "DM", party: "D", incumbent: false, votes: 53 },
]
},
{
id: 5,
level: "local",
title: "New Braunfels City Council At-Large",
type: "special",
date: future(6),
description: "Filling vacant at-large seat",
userVoteId: null,
userThumbsUp: false,
thumbsUp: 22,
candidates: [
{ id: 501, name: "Karen Ellis", initials: "KE", party: "I", incumbent: false, votes: 112 },
{ id: 502, name: "Tom Riggs", initials: "TR", party: "I", incumbent: false, votes: 74 },
{ id: 503, name: "Anita Solis", initials: "AS", party: "I", incumbent: false, votes: 41 },
]
},
// Recent/past
{
id: 6,
level: "state",
title: "Texas Senate District 25",
type: "primary",
date: past(48),
description: "Donna Campbell won unopposed",
userVoteId: 601,
userThumbsUp: true,
thumbsUp: 41,
candidates: [
{ id: 601, name: "Donna Campbell", initials: "DC", party: "R", incumbent: true, votes: 487 },
]
},
{
id: 7,
level: "local",
title: "Comal County Sheriff",
type: "general",
date: past(180),
description: "Mark Reynolds re-elected",
userVoteId: 701,
userThumbsUp: false,
thumbsUp: 19,
candidates: [
{ id: 701, name: "Mark Reynolds", initials: "MR", party: "R", incumbent: true, votes: 623 },
{ id: 702, name: "Phil Sanderson", initials: "PS", party: "D", incumbent: false, votes: 214 },
]
},
]
}
handleVote(electionId, candidateId) {
const election = this.elections.find(e => e.id === electionId)
if (!election || election.userVoteId) return
election.userVoteId = candidateId
const candidate = election.candidates.find(c => c.id === candidateId)
if (candidate) candidate.votes++
this.rerender()
}
handleThumbsUp(electionId) {
const election = this.elections.find(e => e.id === electionId)
if (!election) return
election.userThumbsUp = !election.userThumbsUp
election.thumbsUp += election.userThumbsUp ? 1 : -1
this.rerender()
}
render() {
HStack(() => {
// Sidebar
VStack(() => {
PoliticsSidebar(
this.view,
this.levelFilter,
(v) => { this.view = v; this.rerender(); },
(l) => { this.levelFilter = l; this.rerender(); },
this.jurisdiction,
this.elections
)
})
.width(230, px).minWidth(210, px)
.height(100, pct)
.borderRight("1px solid var(--divider)")
.flexShrink(0).overflow("hidden")
// Main
VStack(() => {
if (this.view === "representatives") {
PoliticsRepresentatives(this.representatives, this.levelFilter)
} else {
PoliticsElections(
this.elections,
this.levelFilter,
(electionId, candidateId) => this.handleVote(electionId, candidateId),
(electionId) => this.handleThumbsUp(electionId)
)
}
})
.flex(1).height(100, pct).overflow("hidden")
})
.height(100, pct).width(100, pct).overflow("hidden")
}
}
register(Politics)

View File

@@ -0,0 +1,3 @@
<svg width="205" height="202" viewBox="0 0 205 202" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M87.127 35.524L76.1095 33.925L71.1823 23.9405C70.0282 21.5864 66.2284 21.5955 65.0743 23.9405L60.147 33.925L49.1207 35.524C47.8334 35.7194 46.7859 36.6077 46.3775 37.8424C45.978 39.0772 46.2976 40.4185 47.2387 41.3424L55.2109 49.1152L53.3288 60.0947C53.1069 61.3827 53.6307 62.6619 54.6872 63.4348C55.2908 63.8701 55.9833 64.0922 56.6935 64.0922C57.2351 64.0922 57.7766 63.9589 58.2827 63.6924L68.137 58.5137L77.9913 63.6924C79.1366 64.2965 80.5215 64.1988 81.5868 63.4348C82.6432 62.662 83.1582 61.3828 82.9451 60.0947L81.063 49.1152L89.0441 41.3336C89.9763 40.4186 90.2959 39.0861 89.8964 37.8424C89.4969 36.5988 88.4404 35.7105 87.1444 35.524H87.127ZM76.2427 45.864L74.991 47.4096L76.5535 56.5148L69.6998 52.9084L67.8532 52.2244L59.7035 56.5149L61.0085 48.8842L61.0884 46.921L54.5011 40.4986L62.1184 39.3882L64.0449 38.873L68.1376 30.5851L71.5379 37.4783L72.6209 39.1661L81.7739 40.4986L76.2607 45.8729L76.2427 45.864ZM155.318 35.524L144.3 33.925L139.364 23.9316C138.21 21.5866 134.384 21.5955 133.256 23.9316L128.329 33.9162L117.303 35.524C116.015 35.7194 114.968 36.6077 114.559 37.8424C114.16 39.0772 114.479 40.4185 115.42 41.3425L123.393 49.1152L121.511 60.0947C121.289 61.3828 121.813 62.6619 122.869 63.4349C123.934 64.2077 125.301 64.3143 126.464 63.6925L136.319 58.5137L146.164 63.6925C146.67 63.959 147.221 64.0922 147.753 64.0922C148.464 64.0922 149.165 63.8701 149.76 63.4349C150.816 62.662 151.331 61.374 151.109 60.1038L149.227 49.1243L157.199 41.3516C158.132 40.4366 158.469 39.1041 158.061 37.8604C157.661 36.6168 156.613 35.7196 155.317 35.5331L155.318 35.524ZM144.407 45.8817L143.182 47.4274L144.735 56.5059L137.882 52.8995L136.035 52.2155L127.885 56.506L129.19 48.8753L129.261 46.9122L122.674 40.4897L130.283 39.3793L132.218 38.8641L136.311 30.5763L139.729 37.4963L140.821 39.1485L149.947 40.4721L144.407 45.873L144.407 45.8817ZM204.545 11.3704H181.534C180.211 4.89449 174.476 0 167.614 0H36.9318C30.0693 0 24.3341 4.89449 23.0114 11.3704H0V17.0556H22.7273V192.773C22.7273 195.731 24.2187 198.423 26.7134 199.995C29.2082 201.559 32.2798 201.745 34.9432 200.457L101.03 168.62C101.811 168.247 102.716 168.238 103.498 168.62L169.593 200.457C170.774 201.026 172.034 201.31 173.295 201.31C174.866 201.31 176.438 200.875 177.832 200.004C180.326 198.423 181.818 195.731 181.818 192.773V17.0556H204.545L204.545 11.3704ZM32.4841 195.338C31.5874 195.765 30.5842 195.711 29.7409 195.187C28.8975 194.654 28.4092 193.775 28.4092 192.78V85.2846H73.8638V175.404L32.4842 195.346L32.4841 195.338ZM102.273 162.649C101.012 162.649 99.7514 162.933 98.5707 163.492L79.5455 172.66V85.2762H125V172.66L105.966 163.492C104.812 162.933 103.551 162.649 102.273 162.649ZM174.814 195.188C173.961 195.712 172.967 195.775 172.07 195.339L130.682 175.397V85.2776H176.136V192.773C176.136 193.768 175.657 194.647 174.814 195.18L174.814 195.188ZM176.136 79.6017H28.4092V14.2221C28.4092 9.52291 32.2355 5.69428 36.9319 5.69428H167.614C172.31 5.69428 176.136 9.52291 176.136 14.2221V79.6017Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -0,0 +1,3 @@
<svg width="205" height="202" viewBox="0 0 205 202" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M87.127 35.524L76.1095 33.925L71.1823 23.9405C70.0282 21.5864 66.2284 21.5955 65.0743 23.9405L60.147 33.925L49.1207 35.524C47.8334 35.7194 46.7859 36.6077 46.3775 37.8424C45.978 39.0772 46.2976 40.4185 47.2387 41.3424L55.2109 49.1152L53.3288 60.0947C53.1069 61.3827 53.6307 62.6619 54.6872 63.4348C55.2908 63.8701 55.9833 64.0922 56.6935 64.0922C57.2351 64.0922 57.7766 63.9589 58.2827 63.6924L68.137 58.5137L77.9913 63.6924C79.1366 64.2965 80.5215 64.1988 81.5868 63.4348C82.6432 62.662 83.1582 61.3828 82.9451 60.0947L81.063 49.1152L89.0441 41.3336C89.9763 40.4186 90.2959 39.0861 89.8964 37.8424C89.4969 36.5988 88.4404 35.7105 87.1444 35.524H87.127ZM76.2427 45.864L74.991 47.4096L76.5535 56.5148L69.6998 52.9084L67.8532 52.2244L59.7035 56.5149L61.0085 48.8842L61.0884 46.921L54.5011 40.4986L62.1184 39.3882L64.0449 38.873L68.1376 30.5851L71.5379 37.4783L72.6209 39.1661L81.7739 40.4986L76.2607 45.8729L76.2427 45.864ZM155.318 35.524L144.3 33.925L139.364 23.9316C138.21 21.5866 134.384 21.5955 133.256 23.9316L128.329 33.9162L117.303 35.524C116.015 35.7194 114.968 36.6077 114.559 37.8424C114.16 39.0772 114.479 40.4185 115.42 41.3425L123.393 49.1152L121.511 60.0947C121.289 61.3828 121.813 62.6619 122.869 63.4349C123.934 64.2077 125.301 64.3143 126.464 63.6925L136.319 58.5137L146.164 63.6925C146.67 63.959 147.221 64.0922 147.753 64.0922C148.464 64.0922 149.165 63.8701 149.76 63.4349C150.816 62.662 151.331 61.374 151.109 60.1038L149.227 49.1243L157.199 41.3516C158.132 40.4366 158.469 39.1041 158.061 37.8604C157.661 36.6168 156.613 35.7196 155.317 35.5331L155.318 35.524ZM144.407 45.8817L143.182 47.4274L144.735 56.5059L137.882 52.8995L136.035 52.2155L127.885 56.506L129.19 48.8753L129.261 46.9122L122.674 40.4897L130.283 39.3793L132.218 38.8641L136.311 30.5763L139.729 37.4963L140.821 39.1485L149.947 40.4721L144.407 45.873L144.407 45.8817ZM204.545 11.3704H181.534C180.211 4.89449 174.476 0 167.614 0H36.9318C30.0693 0 24.3341 4.89449 23.0114 11.3704H0V17.0556H22.7273V192.773C22.7273 195.731 24.2187 198.423 26.7134 199.995C29.2082 201.559 32.2798 201.745 34.9432 200.457L101.03 168.62C101.811 168.247 102.716 168.238 103.498 168.62L169.593 200.457C170.774 201.026 172.034 201.31 173.295 201.31C174.866 201.31 176.438 200.875 177.832 200.004C180.326 198.423 181.818 195.731 181.818 192.773V17.0556H204.545L204.545 11.3704ZM32.4841 195.338C31.5874 195.765 30.5842 195.711 29.7409 195.187C28.8975 194.654 28.4092 193.775 28.4092 192.78V85.2846H73.8638V175.404L32.4842 195.346L32.4841 195.338ZM102.273 162.649C101.012 162.649 99.7514 162.933 98.5707 163.492L79.5455 172.66V85.2762H125V172.66L105.966 163.492C104.812 162.933 103.551 162.649 102.273 162.649ZM174.814 195.188C173.961 195.712 172.967 195.775 172.07 195.339L130.682 175.397V85.2776H176.136V192.773C176.136 193.768 175.657 194.647 174.814 195.18L174.814 195.188ZM176.136 79.6017H28.4092V14.2221C28.4092 9.52291 32.2355 5.69428 36.9319 5.69428H167.614C172.31 5.69428 176.136 9.52291 176.136 14.2221V79.6017Z" fill="#FFE9C8"/>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB