import { customElement, DeesElement, property, html, cssManager, unsafeCSS, css, state, type TemplateResult, } from '@design.estate/dees-element'; import * as plugins from '../../plugins.js'; import * as states from '../../states/accountstate.js'; import { IdpState } from '../../states/idp.state.js'; import { accountDesignTokens } from './sharedstyles.js'; import { CreateOrgModal } from './create-org-modal.js'; import { OrgSelectModal } from './org-select-modal.js'; import { commitinfo } from '../../../ts/00_commitinfo_data.js'; declare global { interface HTMLElementTagNameMap { 'lele-accountnavigation': LeleAccountNavigation; } } @customElement('lele-accountnavigation') export class LeleAccountNavigation extends DeesElement { @state() accessor isGlobalAdmin: boolean = false; @state() accessor currentPath: string = window.location.pathname; constructor() { super(); } private async navigateTo(path: string) { const subrouter = await this.getAccountRouter(); subrouter.pushUrl(path); // Update state after navigation to trigger re-render this.currentPath = window.location.pathname; } private async navigateToOrgPage(page: string) { const currentState = states.accountState.getState(); if (currentState.selectedOrg) { const path = page ? `/org/${currentState.selectedOrg.data.slug}/${page}` : `/org/${currentState.selectedOrg.data.slug}`; await this.navigateTo(path); } else { const targetPath = page ? `/org/:orgName/${page}` : '/org/:orgName'; const description = page ? `Choose an organization to view its ${page}.` : 'Choose an organization to view its overview.'; const result = await OrgSelectModal.show({ targetPath, title: 'Select Organization', description, }); if (result) { await this.navigateTo(result.path.replace('/dash', '')); } } } public static styles = [ cssManager.defaultStyles, accountDesignTokens, css` :host { display: flex; flex-direction: column; background: var(--card); border-right: 1px solid var(--border); height: 100%; } :host([hidden]) { display: none; } .logoArea { padding: 20px 16px; border-bottom: 1px solid var(--border); flex-shrink: 0; } .logo { font-family: 'Cal Sans', 'Geist Sans', sans-serif; letter-spacing: -0.02em; font-size: 20px; font-weight: 600; color: var(--foreground); cursor: pointer; transition: opacity 0.15s ease; display: flex; align-items: center; gap: 8px; } .logo:hover { opacity: 0.8; } .logo idp-icon { opacity: 0.9; } .navContent { flex: 1; overflow-y: auto; padding-bottom: 16px; } .commitinfo { flex-shrink: 0; text-align: center; font-family: 'Geist Mono', monospace; font-size: 10px; padding: 12px 16px; border-top: 1px solid var(--border); color: var(--muted-foreground); opacity: 0.6; background: var(--card); } .navigationGroupLabel { font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; color: var(--muted-foreground); padding: 20px 16px 8px; opacity: 0.7; } .navigationGroupLabel:first-of-type { padding-top: 16px; } .navigationOption { display: flex; align-items: center; gap: 10px; padding: 10px 12px; margin: 2px 8px; border-radius: 8px; font-size: 13px; font-weight: 500; color: var(--muted-foreground); transition: all 0.15s ease; cursor: pointer; } .navigationOption:hover { background: var(--muted); color: var(--foreground); } .navigationOption idp-icon { opacity: 0.7; flex-shrink: 0; } .navigationOption:hover idp-icon { opacity: 1; } .navigationOption.active { background: var(--muted); color: var(--foreground); } .navigationOption.active idp-icon { opacity: 1; } .divider { height: 1px; background: var(--border); margin: 8px 16px; } idp-select { margin: 8px; } `, ]; public async getAccountRouter() { const host = (this.getRootNode() as any).host; return (host as any).subrouter; } public render(): TemplateResult { return html`
v${commitinfo.version}
`; } private renderAdminLink(): TemplateResult | null { if (!this.isGlobalAdmin) { return null; } return html`
`; } private isActive(page: string): boolean { const path = this.currentPath; if (page === '') { // Account overview - exact match return path === '/dash' || path === '/dash/'; } if (page === 'org-overview') { // Org overview - /dash/org/:slug without trailing page type return /^\/dash\/org\/[^\/]+\/?$/.test(path); } // For other pages, check if the path contains the page segment return path.includes(`/${page}`); } public async firstUpdated() { // Listen for popstate (browser back/forward) window.addEventListener('popstate', () => { this.currentPath = window.location.pathname; }); // Watch for URL changes from external navigation (e.g., modals) let lastPath = this.currentPath; const checkPath = () => { if (window.location.pathname !== lastPath) { lastPath = window.location.pathname; this.currentPath = lastPath; } requestAnimationFrame(checkPath); }; requestAnimationFrame(checkPath); const orgSelect = this.shadowRoot.querySelector('idp-select') as plugins.idpCatalog.IdpSelect | null; const orgToMenuEntry = (orgArg?: plugins.idpInterfaces.data.IOrganization): plugins.idpCatalog.IIdpSelectOption | null => { if (!orgArg) { return null; } return { option: orgArg.data.name, key: orgArg.data.slug, payload: orgArg.data.slug, }; }; // "Create new..." option to add at the end const createNewOption = { option: '+ Create new...', key: '__create_new__', payload: '__create_new__', }; states.accountState .select((stateArg) => stateArg.organizations) .pipe( plugins.deesDomtools.plugins.smartrx.rxjs.ops.map((orgArrayArg) => { const orgEntries = orgArrayArg .map(orgToMenuEntry) .filter((entryArg): entryArg is plugins.idpCatalog.IIdpSelectOption => Boolean(entryArg)); // Add "Create new..." at the end return [...orgEntries, createNewOption]; }) ) .subscribe((menuEntries) => { if (orgSelect) { orgSelect.options = menuEntries; } }); states.accountState .select((stateArg) => stateArg.selectedOrg) .pipe(plugins.deesDomtools.plugins.smartrx.rxjs.ops.map(orgToMenuEntry)) .subscribe((selectedOrgArg) => { if (orgSelect) { orgSelect.selectedOption = selectedOrgArg; } }); // Check if user is global admin states.accountState .select((stateArg) => stateArg.user) .subscribe((user) => { this.isGlobalAdmin = user?.data?.isGlobalAdmin ?? false; }); } }