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,250 @@
class SettingsRolesSection extends Shadow {
newRoleName = ""
confirmDeleteId = null
constructor(roles, allApps, activeSection, activeRoleId, roleApps, basePath, callbacks, loading = false) {
super()
this.roles = roles
this.allApps = allApps
this.activeSection = activeSection // "roles" | "role-detail"
this.activeRoleId = activeRoleId
this.roleApps = roleApps
this.basePath = basePath
this.loading = loading
this.onDeleteRole = callbacks.onDeleteRole
this.onCreateRole = callbacks.onCreateRole
this.onToggleApp = callbacks.onToggleApp
}
get activeRole() {
return this.roles.find(r => String(r.id) === String(this.activeRoleId)) ?? null
}
get activeRoleAppIds() {
return this.roleApps[this.activeRoleId] ?? new Set()
}
render() {
if (this.activeSection === "role-detail") {
this.renderRoleDetail()
} else {
this.renderRolesList()
}
}
renderRolesList() {
VStack(() => {
this.backHeader("Roles & Apps", () => window.navigateTo(this.basePath))
VStack(() => {}).height(1, px).background("var(--divider)").flexShrink(0)
if (this.loading) { LoadingCircle(); return }
VStack(() => {
this.roles.forEach((role, i) => {
const appCount = (this.roleApps[role.id] ?? new Set()).size
const isConfirming = this.confirmDeleteId === role.id
HStack(() => {
VStack(() => {
HStack(() => {
p(role.name)
.margin(0).fontSize(0.92, em).fontWeight("600").color("var(--headertext)")
if (role.is_default) {
p("default")
.margin(0).fontSize(0.62, em).fontWeight("600")
.color("var(--quillred)")
.background("rgba(159,28,41,0.1)")
.paddingHorizontal(0.45, em).paddingVertical(0.1, em)
.borderRadius(100, px)
}
})
.gap(0.45, em).alignItems("center")
p(`${appCount} app${appCount !== 1 ? "s" : ""} assigned`)
.margin(0).marginTop(0.15, em).fontSize(0.72, em)
.color("var(--headertext)").opacity(0.4)
})
.flex(1).gap(0)
if (isConfirming) {
HStack(() => {
button("Delete")
.padding("0.25em 0.6em")
.border("none").borderRadius(0.35, em)
.background("var(--quillred)").color("white")
.fontSize(0.72, em).fontWeight("600").cursor("pointer")
.onTap(async (e) => {
e.stopPropagation()
await this.onDeleteRole(role.id)
})
button("✕")
.padding("0.25em 0.45em")
.border("none").borderRadius(0.35, em)
.background("transparent").color("var(--headertext)")
.opacity(0.4).fontSize(0.78, em).cursor("pointer")
.onTap((e) => {
e.stopPropagation()
this.confirmDeleteId = null
this.rerender()
})
})
.gap(0.35, em).alignItems("center")
} else {
HStack(() => {
button("⋯")
.border("none").background("transparent")
.color("var(--headertext)").opacity(0.35)
.fontSize(1.1, em).cursor("pointer").padding("0.1em 0.4em")
.onTap((e) => {
e.stopPropagation()
this.confirmDeleteId = role.id
this.rerender()
})
p("").margin(0).fontSize(1.1, em).color("var(--headertext)").opacity(0.3)
})
.gap(0.1, em).alignItems("center")
}
})
.gap(0.75, em).alignItems("center")
.paddingVertical(0.85, em)
.cursor("pointer")
.onTap(() => window.navigateTo(`${this.basePath}/roles/${role.id}`))
if (i < this.roles.length - 1) {
VStack(() => {}).height(1, px).background("var(--divider)").marginLeft(0)
}
})
// New role input
HStack(() => {
p("+").margin(0).fontSize(1.1, em).color("var(--headertext)").opacity(0.35).flexShrink(0)
input("New role name…", "100%")
.border("none").background("transparent")
.color("var(--headertext)").fontSize(0.88, em).outline("none").flex(1)
.attr({ value: this.newRoleName })
.onInput(e => { this.newRoleName = e.target.value })
.onKeyDown(async (e) => {
if (e.key === "Enter" && this.newRoleName.trim()) {
await this.onCreateRole(this.newRoleName.trim())
this.newRoleName = ""
}
})
})
.gap(0.65, em).alignItems("center")
.paddingVertical(0.85, em)
.borderTop("1px dashed var(--divider)")
.marginTop(0.1, em)
})
.paddingHorizontal(1, em)
.flex(1).overflowY("auto")
})
.height(100, pct).overflow("hidden")
}
renderRoleDetail() {
VStack(() => {
this.backHeader(this.activeRole?.name ?? "Role", () => window.navigateTo(`${this.basePath}/roles`))
VStack(() => {}).height(1, px).background("var(--divider)").flexShrink(0)
if (this.loading) { LoadingCircle(); return }
VStack(() => {
VStack(() => {
p("APP ACCESS")
.margin(0).marginBottom(0.55, em)
.fontSize(0.62, em).fontWeight("700").letterSpacing("0.07em")
.color("var(--headertext)").opacity(0.35)
this.allApps.forEach((app, i) => {
const hasApp = this.activeRoleAppIds.has(app.id)
const comingSoonApps = ["jobs", "politics", "files"]
const isComingSoon = comingSoonApps.includes(app.name)
HStack(() => {
p(app.name.charAt(0).toUpperCase() + app.name.slice(1))
.margin(0).fontSize(0.9, em).color("var(--headertext)").flex(1)
HStack(() => {
if (isComingSoon) {
p("Coming soon")
.margin(0).fontSize(0.9, em).color("var(--headertext)").opacity(0.45)
.background("var(--darkaccent)").border("1px solid var(--divider)")
.paddingHorizontal(0.5, em).paddingVertical(0.15, em).borderRadius(100, px)
}
VStack(() => {})
.width(1.15, em).height(1.15, em)
.borderRadius(0.25, em)
.background(hasApp ? "var(--quillred)" : "transparent")
.border(`2px solid ${hasApp ? "var(--quillred)" : "var(--divider)"}`)
.boxSizing("border-box")
.justifyContent("center").alignItems("center")
})
.justifyContent("center").alignItems("center")
.gap(1, em)
})
.paddingVertical(0.8, em)
.gap(1, em).alignItems("center")
.cursor(isComingSoon ? "default" : "pointer")
.opacity(isComingSoon ? 0.5 : 1)
.onTap(() => { if (!isComingSoon) this.onToggleApp(this.activeRoleId, app.id, !hasApp) })
if (i < this.allApps.length - 1) {
VStack(() => {}).height(1, px).background("var(--divider)")
}
})
})
.paddingHorizontal(1, em)
.paddingVertical(0.75, em)
.marginBottom(1, em)
VStack(() => {}).height(1, px).background("var(--divider)").marginHorizontal(1, em)
VStack(() => {
this.comingSoonRow("🔐", "Permissions")
VStack(() => {}).height(1, px).background("var(--divider)")
this.comingSoonRow("🔔", "Notifications")
VStack(() => {}).height(1, px).background("var(--divider)")
this.comingSoonRow("🎨", "Color & Badge")
})
.paddingHorizontal(1, em)
.marginTop(1, em)
.opacity(0.4)
})
.flex(1).overflowY("auto").paddingBottom(2, em)
})
.height(100, pct).overflow("hidden")
}
backHeader(title, onBack) {
HStack(() => {
button("")
.border("none").background("transparent")
.color("var(--headertext)").fontSize(1.4, em)
.lineHeight("1").paddingVertical(0.2, em).paddingRight(0.3, em)
.cursor("pointer").flexShrink(0)
.onTap(onBack)
p(title)
.margin(0).fontSize(1.05, em).fontWeight("700").color("var(--headertext)")
.flex(1).overflow("hidden").whiteSpace("nowrap").textOverflow("ellipsis")
})
.gap(0.25, em).alignItems("center")
.paddingHorizontal(0.85, em).paddingTop(1.25, em).paddingBottom(0.85, em)
.flexShrink(0)
}
comingSoonRow(icon, label) {
HStack(() => {
p(icon).margin(0).fontSize(1, em).flexShrink(0)
p(label).margin(0).fontSize(0.9, em).color("var(--headertext)").flex(1)
p("Coming soon")
.margin(0).fontSize(0.9, em).color("var(--headertext)").opacity(0.45)
.background("var(--darkaccent)").border("1px solid var(--divider)")
.paddingHorizontal(0.5, em).paddingVertical(0.15, em).borderRadius(100, px)
})
.gap(0.75, em).alignItems("center").paddingVertical(0.85, em)
}
}
register(SettingsRolesSection)