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)