/** * Copyright Aquera Inc 2023 * * This source code is licensed under the BSD-3-Clause license found in the * LICENSE file in the root directory of this source tree. */ import { html, CSSResultArray, TemplateResult, PropertyValues } from 'lit'; import { customElement, property, query, state } from 'lit/decorators.js'; import { ifDefined } from 'lit/directives/if-defined.js'; import { styles } from './nile-app-shell.css'; import { renderGlyph } from '../internal/glyph'; import NileElement from '../internal/nile-element'; import '../nile-button/nile-button'; /** The mode the AI panel is currently in. */ export type NileAppShellPanelMode = 'docked' | 'overlay'; /** Where the top bar is mounted. */ export type NileAppShellTopBarPlacement = 'content' | 'shell'; /** How the main region lays its content out. */ export type NileAppShellContentAlign = 'fluid' | 'centered'; /** The `detail` of the `nile-click` the header back control emits. */ export interface NileAppShellClickDetail { value: 'back'; } /** The shell's own track and region widths, in px. */ interface ShellGeometry { nav: number; rail: number; gutter: number; content: number; aside: number; panel: number; } /** * @summary The application frame every route inherits: a side nav, a content container, an optional aside and an AI assistant panel. * * @status stable * @since 2.0 * * @tag nile-app-shell * * @dependency nile-button * @dependency nile-glyph * * @slot - The page body, inside the content container. * @slot banner - A full-width strip above every other region. * @slot top-bar - The page-title bar, mounted inside the container by default or spanning the shell when `top-bar-placement="shell"`. * @slot top-bar-actions - Controls pinned to the end of the top bar. * @slot tabs - A hoisted second-level tab strip, flush under the top bar and outside `
`'s scroller. * @slot app-icon - The mark shown before the page title whenever this slot has content, clipped to the shell's box. * @slot side-nav - The navigation track. Works best with ``. * @slot aside - A secondary column between the content container and the panel. * @slot panel - The AI panel's body. When empty the panel is unmounted and `openPanel()` is a no-op. * @slot panel-header - Replaces the default panel heading (mark plus title). * @slot panel-actions - Controls added before the panel's close button. * @slot panel-footer - A bar docked below the panel's scroll region, for a composer. * * @event nile-nav-toggle - Emitted after the nav's collapsed state or forced-rail lock changes. `detail: { collapsed, locked }`. * @event nile-panel-toggle - Emitted after the panel opens or closes. `detail: { open, mode }`. * @event nile-panel-mode-change - Emitted when the panel flips between docked and overlay. `detail: { mode }`. * @event nile-click - Emitted when the header's back control is pressed, or `goBackHeader()` is called. `detail: { value: 'back' }`. Emitted on the shell itself, so check `event.target === shell` as well as the value: a `nile-click` from page content bubbles through the shell too. * @event nile-panel-back - Emitted when the panel's back control is pressed, whether the shell's built-in panel chrome or a slotted `nile-app-shell-panel` draws it. * * @csspart skip-link - The skip-to-content link. * @csspart banner - The banner region. * @csspart shell-top-bar - The top bar when it spans the shell. * @csspart top-bar - The top bar, in either placement. * @csspart nav-toggle - The built-in nav toggle button. * @csspart rule - The vertical rule after the nav toggle. * @csspart ask-rule - The vertical rule before the Ask button, drawn when `top-bar-actions` has content. * @csspart top-bar-actions - The end-aligned top bar action group. * @csspart ask - The built-in Ask button, the shell's single door to the panel. * @csspart title-area - The header's own title block: back control, app icon, title, badge and description. * @csspart page-title - The title text, exposed as a heading whose level `headingLevel` sets. * @csspart description - The second line under the title. * @csspart badge - The pill after the title. * @csspart back - The header back control, shown by `headerBack`. * @csspart app-icon - The block holding the `app-icon` slot, shown when the slot has content and sized by `--nile-app-shell-app-icon-size`. * @csspart tabs - The hoisted tab-strip region, under the top bar. * @csspart side-nav - The navigation track. * @csspart content-section - The flex row holding container, aside and panel. * @csspart container - The content container. * @csspart main - The scrolling page body. * @csspart aside - The aside region. * @csspart panel - The AI panel. * @csspart panel-header - The panel's header bar. * @csspart panel-back - The panel's back control, shown by `panelBack`. * @csspart panel-title - The panel's title text. * @csspart panel-actions - The panel's header action group. * @csspart panel-close - The panel's close button. * @csspart panel-body - The panel's scroll region. * @csspart panel-footer - The panel's footer. * * @cssproperty [--nile-app-shell-side-nav-width=240px] - Expanded nav track. * @cssproperty [--nile-app-shell-side-nav-rail-width=70px] - Collapsed track. * @cssproperty [--nile-app-shell-side-nav-bg=transparent] - Surface behind a slotted side nav. * @cssproperty [--nile-app-shell-side-nav-border=0] - Edge on a slotted side nav. * @cssproperty [--nile-app-shell-gutter=4px] - Content-section padding, and the gap between container, aside and panel. * @cssproperty [--nile-app-shell-content-padding=24px] - Container inner padding. * @cssproperty [--nile-app-shell-top-bar-height=68px] - Top bar height. * @cssproperty [--nile-app-shell-panel-header-height=56px] - The panel header's own height. * @cssproperty [--nile-app-shell-tabs-height=33px] - Tab strip height. * @cssproperty [--nile-app-shell-panel-width=480px] - Docked panel width, and the `min()` ceiling for the overlay drawer. * @cssproperty [--nile-app-shell-panel-min-width=360px] - How far a docked panel shrinks to keep pushing rather than covering the page. * @cssproperty [--nile-app-shell-aside-width=320px] - Aside width. * @cssproperty [--nile-app-shell-aside-min-width=240px] - How far the aside shrinks before the page is asked for room. * @cssproperty [--nile-app-shell-content-min-width=640px] - Container floor. * @cssproperty [--nile-app-shell-min-width=768px] - Below this the window scrolls rather than the shell compressing. * @cssproperty [--nile-app-shell-centered-width=824px] - Column width under `content-align="centered"`. * @cssproperty [--nile-app-shell-app-icon-size=20px] - The app icon's box. * @cssproperty [--nile-app-shell-utility-size=32px] - Box the shell's own icon controls sit in. * @cssproperty [--nile-app-shell-radius=16px] - Container, aside and panel radius. * @cssproperty [--nile-app-shell-duration=250ms] - Track and panel motion. * @cssproperty [--nile-app-shell-ease] - Easing for moves and resizes. * * @method openPanel() - Open the AI panel. No-op when the panel is unavailable. * @method closePanel() - Close the AI panel. * @method togglePanel(force?) - Toggle, or force a specific open state. * @method expandNav() - Expand the side nav. * @method collapseNav() - Collapse the side nav to a rail. * @method toggleNav(force?) - Toggle, or force a specific collapsed state. * @method goBackHeader() - Emit `nile-click` with `{ value: 'back' }`, as the header's back control does. * @method goBackPanel() - Emit `nile-panel-back`, as the panel's back control does. * @method goBack() - Deprecated alias of `goBackPanel()`. */ @customElement('nile-app-shell') export class NileAppShell extends NileElement { public static get styles(): CSSResultArray { return [styles]; } /** The user's nav preference. The nav can still render as a rail while this is false — see `navLocked`. */ @property({ type: Boolean, reflect: true, attribute: 'nav-collapsed' }) navCollapsed: boolean = false; /** Whether the AI panel is open. */ @property({ type: Boolean, reflect: true, attribute: 'panel-open' }) panelOpen: boolean = false; /** Unmounts the AI panel for this route even when the `panel` slot is filled; `openPanel()` becomes a no-op. */ @property({ type: Boolean, reflect: true, attribute: 'no-panel' }) noPanel: boolean = false; /** The default panel heading. Ignored when `panel-header` is slotted. */ @property({ type: String, attribute: 'panel-label' }) panelLabel: string = 'Ask Aquera'; /** The page title, rendered as the top bar's own heading (`role="heading"`, level from `headingLevel`) before anything slotted into `top-bar`; leave unset and no title block renders. */ @property({ type: String, attribute: true, reflect: true }) pageTitle?: string; /** The `aria-level` of the `pageTitle` heading, 1–6, so the title can fit the page's own heading outline; e.g. 2 when the page body or banner already has its h1. Out-of-range values clamp to 1–6 and non-numbers fall back to 1. */ @property({ type: Number, attribute: true, reflect: true }) headingLevel: number = 1; /** A second line under the page title; a modifier on `pageTitle` that renders nothing with no title, and takes the bar to 76px. */ @property({ type: String, attribute: true, reflect: true }) description?: string; /** A pill after the page title; a modifier on `pageTitle` like `description`, so with no title there is no badge. */ @property({ type: String, attribute: true, reflect: true }) badge?: string; /** Shows the back arrow before the page title; pressing it emits `nile-click` with `{ value: 'back' }`. Independent of the `app-icon` slot. */ @property({ type: Boolean, attribute: true, reflect: true }) headerBack: boolean = false; /** Accessible name of the header back control. Never rendered as visible text. */ @property({ type: String, attribute: true, reflect: true }) headerBackLabel: string = 'Back'; /** Hides the built-in nav toggle. Mount `nile-app-shell-nav-toggle` yourself. */ @property({ type: Boolean, reflect: true, attribute: 'no-nav-toggle' }) noNavToggle: boolean = false; /** Drops the Cmd/Ctrl+J and Escape panel shortcuts. */ @property({ type: Boolean, attribute: 'no-panel-shortcut' }) noPanelShortcut: boolean = false; /** Hides the skip-to-content link. */ @property({ type: Boolean, reflect: true, attribute: 'no-skip-link' }) noSkipLink: boolean = false; /** Label for the skip-to-content link. */ @property({ type: String, attribute: 'skip-link-label' }) skipLinkLabel: string = 'Skip to main content'; /** Where the top bar is mounted: inside the container, or spanning the shell. */ @property({ type: String, reflect: true, attribute: 'top-bar-placement' }) topBarPlacement: NileAppShellTopBarPlacement = 'content'; /** `centered` caps the page body at `--nile-app-shell-centered-width`. */ @property({ type: String, reflect: true, attribute: 'content-align' }) contentAlign: NileAppShellContentAlign = 'fluid'; /** Makes the slotted page root a size query container, so page CSS can size against the content container instead of the viewport. */ @property({ type: Boolean, reflect: true, attribute: 'content-container' }) contentContainer: boolean = false; /** Drops the container's card chrome for a full-bleed page. */ @property({ type: Boolean, reflect: true }) flat: boolean = false; /** Removes the container's inner padding. */ @property({ type: Boolean, reflect: true, attribute: 'no-padding' }) noPadding: boolean = false; /** The label on the built-in Ask button. */ @property({ type: String, attribute: 'ask-label' }) askLabel: string = 'Ask'; /** Drops the built-in Ask button, for a bar that renders its own. */ @property({ type: Boolean, reflect: true, attribute: 'no-ask' }) noAsk: boolean = false; /** Shows a back control at the start of the panel header for a panel sub-view; emits `nile-panel-back`. */ @property({ type: Boolean, reflect: true }) panelBack: boolean = false; /** Accessible name of the panel's back control. */ @property({ type: String }) panelBackLabel: string = 'Back'; /** Hides the tab strip for this route even when the `tabs` slot is filled. */ @property({ type: Boolean, reflect: true }) noTabs: boolean = false; /** Sizes the shell to the viewport instead of its parent. */ @property({ type: Boolean, reflect: true, attribute: 'full-height' }) fullHeight: boolean = false; /** Below this shell width the panel overlays the content instead of pushing it, and the nav is forced to a rail; zero (default) means never. */ @property({ type: Number, attribute: 'overlay-breakpoint' }) overlayBreakpoint: number = 0; /** Below this shell width an open docked panel forces the nav to a rail. */ @property({ type: Number, attribute: 'docked-rail-breakpoint' }) dockedRailBreakpoint: number = 1440; /** Persists the nav preference and docked panel state under this key. Overlay panel state is never persisted. */ @property({ type: String, attribute: 'persist-key' }) persistKey?: string; /** The panel's current mode. Derived from the shell's width. */ @state() private _panelMode: NileAppShellPanelMode = 'docked'; /** Whether the nav is held at a rail by the shell's width. */ @state() private _navLocked: boolean = false; @state() private _hasBanner: boolean = false; @state() private _hasSideNav: boolean = false; @state() private _hasTopBar: boolean = false; @state() private _hasTopBarActions: boolean = false; @state() private _hasTabs: boolean = false; @state() private _hasAppIcon: boolean = false; @state() private _hasPanel: boolean = false; @state() private _hasPanelFooter: boolean = false; /** True when a `nile-app-shell-panel` is slotted, owning the panel's own chrome so the shell renders a bare region. */ @state() private _panelOwnsChrome: boolean = false; @state() private _hasAside: boolean = false; @query('.main') private _main?: HTMLElement; private _width: number = 0; private _resizeObserver?: ResizeObserver; private _restoredPanelOpen: boolean | null = null; /** The panel's current mode: `docked` or `overlay`. */ public get panelMode(): NileAppShellPanelMode { return this._panelMode; } /** Whether the shell is holding the nav at a rail. The toggle is disabled. */ public get navLocked(): boolean { return this._navLocked; } /** Whether a panel is mounted for this route. */ public get panelAvailable(): boolean { return this._hasPanel && !this.noPanel; } /** Whether the tab strip is rendered: something slotted, and not switched off. */ public get tabsVisible(): boolean { return this._hasTabs && !this.noTabs; } /** Whether the built-in Ask button is rendered. */ public get askVisible(): boolean { return this.panelAvailable && !this.noAsk; } /** Whether the nav renders as a rail right now, preference or lock. */ public get rail(): boolean { return this.navCollapsed || this._navLocked; } public connectedCallback(): void { super.connectedCallback(); this._restore(); this._resizeObserver = new ResizeObserver(entries => { const entry = entries[entries.length - 1]; this._measure(entry.contentRect.width); }); this._resizeObserver.observe(this); document.addEventListener('keydown', this._onKeyDown); } public disconnectedCallback(): void { super.disconnectedCallback(); this._resizeObserver?.disconnect(); this._resizeObserver = undefined; document.removeEventListener('keydown', this._onKeyDown); } protected willUpdate(changed: PropertyValues): void { if ( changed.has('panelOpen') || changed.has('noPanel') || changed.has('overlayBreakpoint') || changed.has('dockedRailBreakpoint') || // Aside/side-nav mounting changes what a docked panel fits beside; ask the map since private state is outside `keyof this`. (changed as Map).has('_hasAside') || (changed as Map).has('_hasSideNav') ) { this._recompute(); } } protected updated(changed: PropertyValues): void { super.updated(changed); // Derived state the stylesheet and consumers read off the host. this.toggleAttribute('rail', this.rail); this.toggleAttribute('nav-locked', this._navLocked); this.toggleAttribute('has-banner', this._hasBanner); this.toggleAttribute('has-side-nav', this._hasSideNav); this.toggleAttribute( 'has-top-bar', this._hasTopBar || this._hasTitleBlock || (!this.noNavToggle && this._hasSideNav) ); // Only with a title to sit under: see _hasTitleBlock. this.toggleAttribute( 'has-description', !!this.description && !!this.pageTitle ); this.toggleAttribute('has-top-bar-actions', this._hasTopBarActions); this.toggleAttribute('has-app-icon', this._hasAppIcon); this.toggleAttribute('has-ask', this.askVisible); this.toggleAttribute('has-tabs', this.tabsVisible); this.toggleAttribute('has-aside', this._hasAside); this.toggleAttribute('has-panel', this.panelAvailable); this.toggleAttribute('has-panel-footer', this._hasPanelFooter); this.setAttribute('panel-mode', this._panelMode); this.setAttribute( 'panel-chrome', this._panelOwnsChrome ? 'external' : 'internal' ); this._syncSideNav(); } /** Open the AI panel. No-op when no panel is mounted for this route. */ public openPanel(): void { if (!this.panelAvailable) return; this._setPanelOpen(true); } /** Close the AI panel. */ public closePanel(): void { this._setPanelOpen(false); } /** Toggle the panel, or force a state. */ public togglePanel(force?: boolean): void { const next = force ?? !this.panelOpen; if (next) this.openPanel(); else this.closePanel(); } /** Emit `nile-click` with `{ value: 'back' }`, as the header's back control does; the page owns the history. */ public goBackHeader(): void { const detail: NileAppShellClickDetail = { value: 'back' }; this.emit('nile-click', detail); } /** Emit `nile-panel-back`, as the panel's back control does; the page owns the view stack. */ public goBackPanel(): void { this.emit('nile-panel-back'); } /** @deprecated Use `goBackPanel()`; kept as an alias that emits `nile-panel-back`. */ public goBack(): void { this.goBackPanel(); } /** Expand the side nav. */ public expandNav(): void { this._setNavCollapsed(false); } /** Collapse the side nav to a rail. */ public collapseNav(): void { this._setNavCollapsed(true); } /** Toggle the side nav, or force a collapsed state. */ public toggleNav(force?: boolean): void { this._setNavCollapsed(force ?? !this.navCollapsed); } /** Move focus to the page body. */ public focusMain(): void { this._main?.focus(); } private _setPanelOpen(value: boolean): void { if (this.panelOpen === value) return; this.panelOpen = value; this._persist(); this.emit('nile-panel-toggle', { open: value, mode: this._panelMode }); } private _setNavCollapsed(value: boolean): void { if (this.navCollapsed === value) return; this.navCollapsed = value; this._persist(); this.emit('nile-nav-toggle', { collapsed: value, locked: this._navLocked, }); } /** Records the shell's width and re-derives everything that depends on it. */ private _measure(width: number): void { if (width === this._width) return; const first = this._width === 0; this._width = width; this._recompute(); // A persisted panel state only applies once we know the panel would dock. if (first && this._restoredPanelOpen !== null) { const open = this._restoredPanelOpen; this._restoredPanelOpen = null; if (open && this._panelMode === 'docked') this.openPanel(); } } /** The shell's geometry, read off its own custom properties, falling back to defaults before a computed style exists. */ private _geometry(): ShellGeometry { const style = getComputedStyle(this); const px = (name: string, fallback: number): number => { const value = parseFloat(style.getPropertyValue(name)); return Number.isFinite(value) ? value : fallback; }; return { // Read the substituted value so they follow the active theme; the literals are the enterprise fallback. nav: px('--nile-app-shell-side-nav-width', 240), rail: px('--nile-app-shell-side-nav-rail-width', 70), gutter: px('--nile-app-shell-gutter', 4), content: px('--nile-app-shell-content-min-width', 640), aside: px('--nile-app-shell-aside-width', 320), panel: px('--nile-app-shell-panel-width', 480), }; } /** The shell width a docked panel needs to sit beside the content floor, over a side-nav `track` wide. */ private _dockRequirement(geo: ShellGeometry, track: number): number { const nav = this._hasSideNav ? track : 0; const aside = this._hasAside ? geo.aside + geo.gutter : 0; /* The content row's chrome is end-edge padding plus one flex gap between container and panel (an aside carries its own gap above), and start-edge padding too when no nav track is there to sit flush against. */ const gutters = this._hasSideNav ? 2 : 3; return nav + geo.content + aside + geo.panel + geo.gutter * gutters; } private _recompute(): void { const width = this._width; const geo = this._geometry(); /* Overlay is opt-in: the shell only overlays below a width the author names, never from running out of room. */ const overlay = this.overlayBreakpoint > 0 && width > 0 && width < this.overlayBreakpoint; const mode: NileAppShellPanelMode = overlay ? 'overlay' : 'docked'; // A docked panel squeezes the container, so a narrow shell trades the expanded nav for content width, measured against the expanded track. const railAt = Math.max( this.dockedRailBreakpoint, this._dockRequirement(geo, geo.nav) ); const squeezed = !overlay && this.panelOpen && this.panelAvailable && width > 0 && width < railAt; const locked = overlay || squeezed; if (mode !== this._panelMode) { this._panelMode = mode; this.emit('nile-panel-mode-change', { mode }); } if (locked !== this._navLocked) { this._navLocked = locked; this.emit('nile-nav-toggle', { collapsed: this.navCollapsed, locked, }); } } /** Mirrors the effective rail state onto a slotted `nile-side-bar`. */ private _syncSideNav(): void { this.querySelectorAll('[slot="side-nav"]').forEach(el => { if (el.tagName === 'NILE-SIDE-BAR') el.toggleAttribute('collapsed', this.rail); }); } /** A slotted sidebar's own expand button is the same toggle. */ private _onSideNavToggle = (event: Event): void => { const detail = (event as CustomEvent).detail; if (detail && typeof detail.collapsed === 'boolean') { this._setNavCollapsed(detail.collapsed); } }; private _onKeyDown = (event: KeyboardEvent): void => { if (this.noPanelShortcut || !this.panelAvailable) return; const key = event.key ?? ''; if ((event.metaKey || event.ctrlKey) && key.toLowerCase() === 'j') { event.preventDefault(); this.togglePanel(); return; } // A docked panel is furniture, not a dismissible overlay. if (key === 'Escape' && this.panelOpen && this._panelMode === 'overlay') { this.closePanel(); } }; private _storageKey(part: string): string { return `nile-app-shell:${this.persistKey}:${part}`; } private _restore(): void { if (!this.persistKey) return; try { const nav = localStorage.getItem(this._storageKey('nav-collapsed')); if (nav !== null) this.navCollapsed = nav === 'true'; const panel = localStorage.getItem(this._storageKey('panel-open')); if (panel !== null) this._restoredPanelOpen = panel === 'true'; } catch { /* Storage can be blocked; the shell still works without it. */ } } private _persist(): void { if (!this.persistKey) return; try { localStorage.setItem( this._storageKey('nav-collapsed'), String(this.navCollapsed) ); if (this._panelMode === 'docked') { localStorage.setItem( this._storageKey('panel-open'), String(this.panelOpen) ); } } catch { /* Storage can be blocked; the shell still works without it. */ } } private _onSlotChange(slot: string, event: Event): void { const assigned = (event.target as HTMLSlotElement).assignedNodes({ flatten: true, }); const filled = assigned.some( node => node.nodeType === Node.ELEMENT_NODE || (node.textContent ?? '').trim() !== '' ); switch (slot) { case 'banner': this._hasBanner = filled; break; case 'side-nav': this._hasSideNav = filled; this._syncSideNav(); break; case 'top-bar': this._hasTopBar = filled; break; case 'top-bar-actions': this._hasTopBarActions = filled; break; case 'tabs': this._hasTabs = filled; break; case 'app-icon': this._hasAppIcon = filled; break; case 'aside': this._hasAside = filled; break; case 'panel': this._hasPanel = filled; this._panelOwnsChrome = assigned.some( node => node.nodeType === Node.ELEMENT_NODE && (node as Element).tagName === 'NILE-APP-SHELL-PANEL' ); this._recompute(); break; case 'panel-footer': this._hasPanelFooter = filled; break; default: break; } } private _renderNavToggle(): TemplateResult | string { if (this.noNavToggle || !this._hasSideNav) return ''; const expanded = !this.rail; return html` `; } /** Whether the header has a title block of its own to draw: a title, the back arrow or a slotted app icon mounts it. */ private get _hasTitleBlock(): boolean { return !!this.pageTitle || this.headerBack || this._hasAppIcon; } /** `headingLevel` as a safe `aria-level`: a whole number clamped to 1–6, and 1 for anything non-numeric. */ private get _headingLevel(): number { const level = Math.trunc(Number(this.headingLevel)); return Number.isFinite(level) ? Math.min(6, Math.max(1, level)) : 1; } private _renderBack(): TemplateResult | string { if (!this.headerBack) return ''; return html` `; } private _renderAppIconSlot(): TemplateResult { return html` this._onSlotChange('app-icon', e)} >`; } /* The app-icon slot has to exist whenever the stack does not, or a slotted icon could never mount it: park it, hidden. */ private _renderAppIconProbe(): TemplateResult { return html``; } private _renderTitleBlock(): TemplateResult { if (!this._hasTitleBlock) return this._renderAppIconProbe(); /* The stack is the title and/or the app icon; a back arrow on its own brings no empty stack. */ if (!this.pageTitle && !this._hasAppIcon) { return html`
${this._renderBack()} ${this._renderAppIconProbe()}
`; } /* The back arrow sits beside the stack; the app icon starts the stack's first row, so the description lines up under the icon when there is one and under the title when not. */ return html`
${this._renderBack()}
${this._renderAppIconSlot()} ${this.pageTitle ? html`
${this.pageTitle}
` : ''} ${this.pageTitle && this.badge ? html`${this.badge}` : ''}
${this.pageTitle && this.description ? html`

${this.description}

` : ''}
`; } private _renderTopBar( placement: NileAppShellTopBarPlacement ): TemplateResult { return html`
${this._renderNavToggle()}
${this._renderTitleBlock()} this._onSlotChange('top-bar', e)} >
this._onSlotChange('top-bar-actions', e)} > ${this.askVisible && this._hasTopBarActions ? html`` : ''} ${this.askVisible ? html` this.openPanel()} > ${renderGlyph('ng-sparkles', 16, 'currentColor')} ${this.askLabel} ` : ''}
`; } public render(): TemplateResult { const shellBar = this.topBarPlacement === 'shell'; return html` ${shellBar ? this._renderTopBar('shell') : ''}
this._onSlotChange('side-nav', e)} >
${shellBar ? '' : this._renderTopBar('content')}
this._onSlotChange('tabs', e)} >
this._onSlotChange('aside', e)} >
`; } } export default NileAppShell; declare global { interface HTMLElementTagNameMap { 'nile-app-shell': NileAppShell; } }